Skip to content

Commit 6ea1cf6

Browse files
committed
compose [nfc]: Make variant of _ContentInput without the typing notifier
We'll use this for the edit-message compose box. Done by making _ContentInput and _ContentInputState into abstract classes, with subclasses implementing versions with and without the typing-notifier logic.
1 parent 16e4d88 commit 6ea1cf6

File tree

1 file changed

+131
-79
lines changed

1 file changed

+131
-79
lines changed

lib/widgets/compose_box.dart

Lines changed: 131 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -394,24 +394,136 @@ class ComposeContentController extends ComposeController<ContentValidationError>
394394
}
395395
}
396396

397-
class _ContentInput extends StatefulWidget {
398-
const _ContentInput({
397+
abstract class _ContentInput extends StatefulWidget {
398+
/// A content input that operates [PerAccountStore.typingNotifier]
399+
/// to send the self-user's typing-status updates.
400+
///
401+
/// The typing-status updates will be sent to [destination].
402+
factory _ContentInput.withTypingNotifier({
403+
required Narrow narrow,
404+
required SendableNarrow destination,
405+
required ComposeBoxController controller,
406+
required String hintText,
407+
}) => _ContentInputWithTypingNotifier._(
408+
narrow: narrow,
409+
destination: destination,
410+
controller: controller,
411+
hintText: hintText,
412+
);
413+
414+
// We'll use this soon.
415+
// ignore: unused_element
416+
factory _ContentInput.noTypingNotifier({
417+
required Narrow narrow,
418+
required ComposeBoxController controller,
419+
required String hintText,
420+
}) => _ContentInputNoTypingNotifier._(
421+
narrow: narrow,
422+
controller: controller,
423+
hintText: hintText,
424+
);
425+
426+
const _ContentInput._({
399427
required this.narrow,
400-
required this.destination,
401428
required this.controller,
402429
required this.hintText,
403430
});
404431

405432
final Narrow narrow;
406-
final SendableNarrow destination;
407433
final ComposeBoxController controller;
408434
final String hintText;
435+
}
436+
437+
abstract class _ContentInputState<T extends _ContentInput> extends State<T> {
438+
static double maxHeight(BuildContext context) {
439+
final clampingTextScaler = MediaQuery.textScalerOf(context)
440+
.clamp(maxScaleFactor: 1.5);
441+
final scaledLineHeight = clampingTextScaler.scale(_fontSize) * _lineHeightRatio;
442+
443+
// Reserve space to fully show the first 7th lines and just partially
444+
// clip the 8th line, where the height matches the spec at
445+
// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=3960-5147&node-type=text&m=dev
446+
// > Maximum size of the compose box is suggested to be 178px. Which
447+
// > has 7 fully visible lines of text
448+
//
449+
// The partial line hints that the content input is scrollable.
450+
//
451+
// Using the ambient TextScale means this works for different values of the
452+
// system text-size setting. We clamp to a max scale factor to limit
453+
// how tall the content input can get; that's to save room for the message
454+
// list. The user can still scroll the input to see everything.
455+
return _verticalPadding + 7.727 * scaledLineHeight;
456+
}
457+
458+
static const _verticalPadding = 8.0;
459+
static const _fontSize = 17.0;
460+
static const _lineHeight = 22.0;
461+
static const _lineHeightRatio = _lineHeight / _fontSize;
462+
463+
@override
464+
Widget build(BuildContext context) {
465+
final designVariables = DesignVariables.of(context);
466+
467+
return ComposeAutocomplete(
468+
narrow: widget.narrow,
469+
controller: widget.controller.content,
470+
focusNode: widget.controller.contentFocusNode,
471+
fieldViewBuilder: (context) => ConstrainedBox(
472+
constraints: BoxConstraints(maxHeight: maxHeight(context)),
473+
// This [ClipRect] replaces the [TextField] clipping we disable below.
474+
child: ClipRect(
475+
child: InsetShadowBox(
476+
top: _verticalPadding, bottom: _verticalPadding,
477+
color: designVariables.composeBoxBg,
478+
child: TextField(
479+
controller: widget.controller.content,
480+
focusNode: widget.controller.contentFocusNode,
481+
// Let the content show through the `contentPadding` so that
482+
// our [InsetShadowBox] can fade it smoothly there.
483+
clipBehavior: Clip.none,
484+
style: TextStyle(
485+
fontSize: _fontSize,
486+
height: _lineHeightRatio,
487+
color: designVariables.textInput),
488+
// From the spec at
489+
// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=3960-5147&node-type=text&m=dev
490+
// > Compose box has the height to fit 2 lines. This is [done] to
491+
// > have a bigger hit area for the user to start the input. […]
492+
minLines: 2,
493+
maxLines: null,
494+
textCapitalization: TextCapitalization.sentences,
495+
decoration: InputDecoration(
496+
// This padding ensures that the user can always scroll long
497+
// content entirely out of the top or bottom shadow if desired.
498+
// With this and the `minLines: 2` above, an empty content input
499+
// gets 60px vertical distance (with no text-size scaling)
500+
// between the top of the top shadow and the bottom of the
501+
// bottom shadow. That's a bit more than the 54px given in the
502+
// Figma, and we can revisit if needed, but it's tricky to get
503+
// that 54px distance while also making the scrolling work like
504+
// this and offering two lines of touchable area.
505+
contentPadding: const EdgeInsets.symmetric(vertical: _verticalPadding),
506+
hintText: widget.hintText,
507+
hintStyle: TextStyle(
508+
color: designVariables.textInput.withFadedAlpha(0.5))))))));
509+
}
510+
}
511+
512+
class _ContentInputWithTypingNotifier extends _ContentInput {
513+
const _ContentInputWithTypingNotifier._({
514+
required super.narrow,
515+
required this.destination,
516+
required super.controller,
517+
required super.hintText,
518+
}) : super._();
519+
520+
final SendableNarrow destination;
409521

410522
@override
411-
State<_ContentInput> createState() => _ContentInputState();
523+
State<_ContentInput> createState() => _ContentInputStateWithTypingNotifier();
412524
}
413525

414-
class _ContentInputState extends State<_ContentInput> with WidgetsBindingObserver {
526+
class _ContentInputStateWithTypingNotifier extends _ContentInputState<_ContentInputWithTypingNotifier> with WidgetsBindingObserver {
415527
@override
416528
void initState() {
417529
super.initState();
@@ -421,7 +533,7 @@ class _ContentInputState extends State<_ContentInput> with WidgetsBindingObserve
421533
}
422534

423535
@override
424-
void didUpdateWidget(covariant _ContentInput oldWidget) {
536+
void didUpdateWidget(covariant _ContentInputWithTypingNotifier oldWidget) {
425537
super.didUpdateWidget(oldWidget);
426538
if (widget.controller != oldWidget.controller) {
427539
oldWidget.controller.content.removeListener(_contentChanged);
@@ -483,81 +595,21 @@ class _ContentInputState extends State<_ContentInput> with WidgetsBindingObserve
483595
case AppLifecycleState.resumed:
484596
}
485597
}
598+
}
486599

487-
static double maxHeight(BuildContext context) {
488-
final clampingTextScaler = MediaQuery.textScalerOf(context)
489-
.clamp(maxScaleFactor: 1.5);
490-
final scaledLineHeight = clampingTextScaler.scale(_fontSize) * _lineHeightRatio;
491-
492-
// Reserve space to fully show the first 7th lines and just partially
493-
// clip the 8th line, where the height matches the spec at
494-
// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=3960-5147&node-type=text&m=dev
495-
// > Maximum size of the compose box is suggested to be 178px. Which
496-
// > has 7 fully visible lines of text
497-
//
498-
// The partial line hints that the content input is scrollable.
499-
//
500-
// Using the ambient TextScale means this works for different values of the
501-
// system text-size setting. We clamp to a max scale factor to limit
502-
// how tall the content input can get; that's to save room for the message
503-
// list. The user can still scroll the input to see everything.
504-
return _verticalPadding + 7.727 * scaledLineHeight;
505-
}
506-
507-
static const _verticalPadding = 8.0;
508-
static const _fontSize = 17.0;
509-
static const _lineHeight = 22.0;
510-
static const _lineHeightRatio = _lineHeight / _fontSize;
600+
class _ContentInputNoTypingNotifier extends _ContentInput {
601+
const _ContentInputNoTypingNotifier._({
602+
required super.narrow,
603+
required super.controller,
604+
required super.hintText,
605+
}) : super._();
511606

512607
@override
513-
Widget build(BuildContext context) {
514-
final designVariables = DesignVariables.of(context);
515-
516-
return ComposeAutocomplete(
517-
narrow: widget.narrow,
518-
controller: widget.controller.content,
519-
focusNode: widget.controller.contentFocusNode,
520-
fieldViewBuilder: (context) => ConstrainedBox(
521-
constraints: BoxConstraints(maxHeight: maxHeight(context)),
522-
// This [ClipRect] replaces the [TextField] clipping we disable below.
523-
child: ClipRect(
524-
child: InsetShadowBox(
525-
top: _verticalPadding, bottom: _verticalPadding,
526-
color: designVariables.composeBoxBg,
527-
child: TextField(
528-
controller: widget.controller.content,
529-
focusNode: widget.controller.contentFocusNode,
530-
// Let the content show through the `contentPadding` so that
531-
// our [InsetShadowBox] can fade it smoothly there.
532-
clipBehavior: Clip.none,
533-
style: TextStyle(
534-
fontSize: _fontSize,
535-
height: _lineHeightRatio,
536-
color: designVariables.textInput),
537-
// From the spec at
538-
// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=3960-5147&node-type=text&m=dev
539-
// > Compose box has the height to fit 2 lines. This is [done] to
540-
// > have a bigger hit area for the user to start the input. […]
541-
minLines: 2,
542-
maxLines: null,
543-
textCapitalization: TextCapitalization.sentences,
544-
decoration: InputDecoration(
545-
// This padding ensures that the user can always scroll long
546-
// content entirely out of the top or bottom shadow if desired.
547-
// With this and the `minLines: 2` above, an empty content input
548-
// gets 60px vertical distance (with no text-size scaling)
549-
// between the top of the top shadow and the bottom of the
550-
// bottom shadow. That's a bit more than the 54px given in the
551-
// Figma, and we can revisit if needed, but it's tricky to get
552-
// that 54px distance while also making the scrolling work like
553-
// this and offering two lines of touchable area.
554-
contentPadding: const EdgeInsets.symmetric(vertical: _verticalPadding),
555-
hintText: widget.hintText,
556-
hintStyle: TextStyle(
557-
color: designVariables.textInput.withFadedAlpha(0.5))))))));
558-
}
608+
State<_ContentInput> createState() => _ContentInputStateNoTypingNotifier();
559609
}
560610

611+
class _ContentInputStateNoTypingNotifier extends _ContentInputState<_ContentInputNoTypingNotifier> {}
612+
561613
/// The content input for _StreamComposeBox.
562614
class _StreamContentInput extends StatefulWidget {
563615
const _StreamContentInput({required this.narrow, required this.controller});
@@ -645,7 +697,7 @@ class _StreamContentInputState extends State<_StreamContentInput> {
645697
// ignore: dead_null_aware_expression // null topic names soon to be enabled
646698
: '#$streamName > ${hintTopic.displayName ?? store.realmEmptyTopicDisplayName}';
647699

648-
return _ContentInput(
700+
return _ContentInput.withTypingNotifier(
649701
narrow: widget.narrow,
650702
destination: TopicNarrow(widget.narrow.streamId,
651703
TopicName(widget.controller.topic.textNormalized)),
@@ -732,7 +784,7 @@ class _FixedDestinationContentInput extends StatelessWidget {
732784

733785
@override
734786
Widget build(BuildContext context) {
735-
return _ContentInput(
787+
return _ContentInput.withTypingNotifier(
736788
narrow: narrow,
737789
destination: narrow,
738790
controller: controller,

0 commit comments

Comments
 (0)