сын не ест камни(
This commit is contained in:
@@ -58,6 +58,7 @@ class ComposerInputBar extends StatelessWidget {
|
|||||||
this.forceSend = false,
|
this.forceSend = false,
|
||||||
this.hintText = 'Message',
|
this.hintText = 'Message',
|
||||||
this.bottomSafe = true,
|
this.bottomSafe = true,
|
||||||
|
this.vignette = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String chatType;
|
final String chatType;
|
||||||
@@ -96,6 +97,7 @@ class ComposerInputBar extends StatelessWidget {
|
|||||||
final bool forceSend;
|
final bool forceSend;
|
||||||
final String hintText;
|
final String hintText;
|
||||||
final bool bottomSafe;
|
final bool bottomSafe;
|
||||||
|
final bool vignette;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -195,9 +197,9 @@ class ComposerInputBar extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
_messagePreview(cs, forwards),
|
_messagePreview(cs, forwards),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: EdgeInsets.symmetric(
|
||||||
horizontal: 12.0,
|
horizontal: _barSideInset,
|
||||||
vertical: 8.0,
|
vertical: _barVerticalInset,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
@@ -205,8 +207,8 @@ class ComposerInputBar extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
constraints: const BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
minHeight: 54,
|
minHeight: _controlSize,
|
||||||
maxHeight: 180,
|
maxHeight: 180,
|
||||||
),
|
),
|
||||||
child: _fieldSurface(
|
child: _fieldSurface(
|
||||||
@@ -215,95 +217,119 @@ class ComposerInputBar extends StatelessWidget {
|
|||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
AnimatedBuilder(
|
AnimatedBuilder(
|
||||||
animation: attachAnim,
|
animation: Listenable.merge([
|
||||||
|
voiceRec.isRecording,
|
||||||
|
note.isRecording,
|
||||||
|
]),
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
final t = attachAnim.value;
|
final recording =
|
||||||
|
voiceRec.isRecording.value ||
|
||||||
|
note.isRecording.value;
|
||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
ignoring: t > 0.5,
|
ignoring: recording,
|
||||||
child: Opacity(
|
child: AnimatedOpacity(
|
||||||
opacity: (1 - t).clamp(0.0, 1.0),
|
opacity: recording ? 0 : 1,
|
||||||
|
duration: const Duration(milliseconds: 180),
|
||||||
|
curve: Curves.easeOut,
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: AnimatedBuilder(
|
||||||
padding: const EdgeInsets.symmetric(
|
animation: attachAnim,
|
||||||
horizontal: 14,
|
builder: (context, child) {
|
||||||
),
|
final t = attachAnim.value;
|
||||||
child: Row(
|
return IgnorePointer(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
ignoring: t > 0.5,
|
||||||
children: [
|
child: Opacity(
|
||||||
if (showStickerButton) ...[
|
opacity: (1 - t).clamp(0.0, 1.0),
|
||||||
GestureDetector(
|
child: child,
|
||||||
behavior: HitTestBehavior.opaque,
|
),
|
||||||
onTap: onToggleStickerPanel,
|
);
|
||||||
child: Icon(
|
},
|
||||||
Symbols.face,
|
child: Padding(
|
||||||
color: mutedIcon,
|
padding: EdgeInsets.only(
|
||||||
size: 24,
|
left: _fieldSideInset,
|
||||||
weight: 400,
|
right: _fieldTrailingInset,
|
||||||
),
|
),
|
||||||
),
|
child: Row(
|
||||||
const SizedBox(width: 12),
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
],
|
children: [
|
||||||
Expanded(
|
if (showStickerButton) ...[
|
||||||
child: Focus(
|
GestureDetector(
|
||||||
onKeyEvent: (node, event) {
|
behavior: HitTestBehavior.opaque,
|
||||||
if (event is KeyDownEvent &&
|
onTap: onToggleStickerPanel,
|
||||||
event.logicalKey ==
|
child: Icon(
|
||||||
LogicalKeyboardKey.enter &&
|
Symbols.face,
|
||||||
!HardwareKeyboard
|
color: mutedIcon,
|
||||||
.instance
|
size: 24,
|
||||||
.isShiftPressed) {
|
weight: 400,
|
||||||
if (hasText.value ||
|
|
||||||
hasForward ||
|
|
||||||
forceSend) {
|
|
||||||
onSendText();
|
|
||||||
}
|
|
||||||
return KeyEventResult.handled;
|
|
||||||
}
|
|
||||||
return KeyEventResult.ignored;
|
|
||||||
},
|
|
||||||
child: TextField(
|
|
||||||
controller: messageController,
|
|
||||||
focusNode: messageFocusNode,
|
|
||||||
style: TextStyle(
|
|
||||||
color: cs.onSurface,
|
|
||||||
fontSize: 16,
|
|
||||||
),
|
),
|
||||||
maxLines: null,
|
),
|
||||||
keyboardType: TextInputType.multiline,
|
const SizedBox(width: 12),
|
||||||
textCapitalization:
|
],
|
||||||
TextCapitalization.sentences,
|
Expanded(
|
||||||
textAlignVertical:
|
child: Focus(
|
||||||
TextAlignVertical.center,
|
onKeyEvent: (node, event) {
|
||||||
contextMenuBuilder: contextMenuBuilder,
|
if (event is KeyDownEvent &&
|
||||||
decoration: InputDecoration(
|
event.logicalKey ==
|
||||||
hintText: hintText,
|
LogicalKeyboardKey.enter &&
|
||||||
hintStyle: TextStyle(
|
!HardwareKeyboard
|
||||||
color: cs.onSurfaceVariant,
|
.instance
|
||||||
|
.isShiftPressed) {
|
||||||
|
if (hasText.value ||
|
||||||
|
hasForward ||
|
||||||
|
forceSend) {
|
||||||
|
onSendText();
|
||||||
|
}
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
},
|
||||||
|
child: TextField(
|
||||||
|
controller: messageController,
|
||||||
|
focusNode: messageFocusNode,
|
||||||
|
style: TextStyle(
|
||||||
|
color: cs.onSurface,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
),
|
),
|
||||||
border: InputBorder.none,
|
maxLines: null,
|
||||||
isDense: true,
|
keyboardType: TextInputType.multiline,
|
||||||
contentPadding:
|
textCapitalization:
|
||||||
const EdgeInsets.symmetric(
|
TextCapitalization.sentences,
|
||||||
vertical: 14,
|
textAlignVertical:
|
||||||
),
|
TextAlignVertical.center,
|
||||||
|
contextMenuBuilder:
|
||||||
|
contextMenuBuilder,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: hintText,
|
||||||
|
hintStyle: TextStyle(
|
||||||
|
color: cs.onSurfaceVariant,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
border: InputBorder.none,
|
||||||
|
isDense: true,
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(
|
||||||
|
vertical: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
if (showAttachButton)
|
||||||
if (showAttachButton)
|
_AttachButton(
|
||||||
_AttachButton(
|
hasText: hasText,
|
||||||
hasText: hasText,
|
onOpen: onOpenAttach,
|
||||||
onOpen: onOpenAttach,
|
onLongOpen: onOpenAttachScheduled,
|
||||||
onLongOpen: onOpenAttachScheduled,
|
uploadStatus: uploadStatus,
|
||||||
uploadStatus: uploadStatus,
|
mutedIcon: mutedIcon,
|
||||||
mutedIcon: mutedIcon,
|
cs: cs,
|
||||||
cs: cs,
|
slot: _attachSlot,
|
||||||
),
|
leading: _attachLeading,
|
||||||
],
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -312,7 +338,7 @@ class ComposerInputBar extends StatelessWidget {
|
|||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 54,
|
height: _controlSize,
|
||||||
child: AnimatedBuilder(
|
child: AnimatedBuilder(
|
||||||
animation: attachAnim,
|
animation: attachAnim,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
@@ -385,7 +411,7 @@ class ComposerInputBar extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 8),
|
SizedBox(width: _actionGap),
|
||||||
AnimatedBuilder(
|
AnimatedBuilder(
|
||||||
animation: attachAnim,
|
animation: attachAnim,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
@@ -458,8 +484,8 @@ class ComposerInputBar extends StatelessWidget {
|
|||||||
? onScheduleMessage
|
? onScheduleMessage
|
||||||
: null,
|
: null,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 54,
|
width: _controlSize,
|
||||||
height: 54,
|
height: _controlSize,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: ComposerMorphIcon(
|
child: ComposerMorphIcon(
|
||||||
action: sendMode
|
action: sendMode
|
||||||
@@ -543,12 +569,29 @@ class ComposerInputBar extends StatelessWidget {
|
|||||||
|
|
||||||
bool get _translucent => _frost || _liquid;
|
bool get _translucent => _frost || _liquid;
|
||||||
|
|
||||||
|
double get _controlSize => _flat ? 48 : 54;
|
||||||
|
|
||||||
|
double get _barSideInset => _flat ? 0 : 12;
|
||||||
|
|
||||||
|
double get _barVerticalInset => _flat ? 4 : 8;
|
||||||
|
|
||||||
|
double get _fieldSideInset => _flat ? 12 : 14;
|
||||||
|
|
||||||
|
double get _fieldTrailingInset => _flat ? 0 : 14;
|
||||||
|
|
||||||
|
double get _actionGap => _flat ? 0 : 8;
|
||||||
|
|
||||||
|
double get _attachSlot => _flat ? 48 : 36;
|
||||||
|
|
||||||
|
double get _attachLeading => _flat ? 0 : 12;
|
||||||
|
|
||||||
Widget _barSurface(ColorScheme cs, Widget child) {
|
Widget _barSurface(ColorScheme cs, Widget child) {
|
||||||
if (!_flat || _translucent) return child;
|
if (!_flat || _translucent) return child;
|
||||||
|
if (chrome == ChatChromeStyle.blur) return child;
|
||||||
return DecoratedBox(
|
return DecoratedBox(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: cs.surface,
|
color: cs.surface,
|
||||||
border: Border(top: AppFrost.hairline(cs)),
|
border: vignette ? null : Border(top: AppFrost.hairline(cs)),
|
||||||
),
|
),
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
@@ -614,7 +657,7 @@ class ComposerInputBar extends StatelessWidget {
|
|||||||
blurSigma: _frost ? AppFrost.sigma : null,
|
blurSigma: _frost ? AppFrost.sigma : null,
|
||||||
liquid: _liquid,
|
liquid: _liquid,
|
||||||
backdropKey: backdropKey,
|
backdropKey: backdropKey,
|
||||||
borderRadius: BorderRadius.circular(27),
|
borderRadius: BorderRadius.circular(_controlSize / 2),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
onLongPress: onLongPress,
|
onLongPress: onLongPress,
|
||||||
keepInkLayer: true,
|
keepInkLayer: true,
|
||||||
@@ -805,8 +848,8 @@ class ComposerInputBar extends StatelessWidget {
|
|||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 27 - glow / 2,
|
left: _controlSize / 2 - glow / 2,
|
||||||
top: 27 - glow / 2,
|
top: _controlSize / 2 - glow / 2,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: glow,
|
width: glow,
|
||||||
height: glow,
|
height: glow,
|
||||||
@@ -845,7 +888,7 @@ class ComposerInputBar extends StatelessWidget {
|
|||||||
ValueListenable<double> lockDrag,
|
ValueListenable<double> lockDrag,
|
||||||
) {
|
) {
|
||||||
return Positioned(
|
return Positioned(
|
||||||
bottom: 62,
|
bottom: _controlSize + 8,
|
||||||
child: ValueListenableBuilder<double>(
|
child: ValueListenableBuilder<double>(
|
||||||
valueListenable: lockDrag,
|
valueListenable: lockDrag,
|
||||||
builder: (context, lock, _) => Opacity(
|
builder: (context, lock, _) => Opacity(
|
||||||
@@ -890,12 +933,8 @@ class ComposerInputBar extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _recordingIndicator(ColorScheme cs, bool video) {
|
Widget _recordingIndicator(ColorScheme cs, bool video) {
|
||||||
return Container(
|
return Padding(
|
||||||
color: Color.alphaBlend(
|
padding: EdgeInsets.symmetric(horizontal: _fieldSideInset),
|
||||||
cs.surfaceContainerHighest.withValues(alpha: 0.92),
|
|
||||||
cs.surface,
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
if (video)
|
if (video)
|
||||||
@@ -1016,6 +1055,8 @@ class _AttachButton extends StatelessWidget {
|
|||||||
final ValueListenable<UploadStatus> uploadStatus;
|
final ValueListenable<UploadStatus> uploadStatus;
|
||||||
final Color mutedIcon;
|
final Color mutedIcon;
|
||||||
final ColorScheme cs;
|
final ColorScheme cs;
|
||||||
|
final double slot;
|
||||||
|
final double leading;
|
||||||
|
|
||||||
const _AttachButton({
|
const _AttachButton({
|
||||||
required this.hasText,
|
required this.hasText,
|
||||||
@@ -1024,6 +1065,8 @@ class _AttachButton extends StatelessWidget {
|
|||||||
required this.uploadStatus,
|
required this.uploadStatus,
|
||||||
required this.mutedIcon,
|
required this.mutedIcon,
|
||||||
required this.cs,
|
required this.cs,
|
||||||
|
required this.slot,
|
||||||
|
required this.leading,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -1043,7 +1086,7 @@ class _AttachButton extends StatelessWidget {
|
|||||||
final onLongPress = disabled ? null : onLongOpen;
|
final onLongPress = disabled ? null : onLongOpen;
|
||||||
return AnimatedContainer(
|
return AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
width: isText ? 0 : 36,
|
width: isText ? 0 : slot,
|
||||||
child: AnimatedOpacity(
|
child: AnimatedOpacity(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
opacity: isText ? 0 : 1,
|
opacity: isText ? 0 : 1,
|
||||||
@@ -1054,7 +1097,7 @@ class _AttachButton extends StatelessWidget {
|
|||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
onLongPress: onLongPress,
|
onLongPress: onLongPress,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(left: 12),
|
padding: EdgeInsets.only(left: leading),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -581,6 +581,9 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
static const double _glossyHeaderHeight = 76.0;
|
static const double _glossyHeaderHeight = 76.0;
|
||||||
static const double _glossySearchHeight = 58.0;
|
static const double _glossySearchHeight = 58.0;
|
||||||
static const double _pinnedBannerLift = 6.0;
|
static const double _pinnedBannerLift = 6.0;
|
||||||
|
static const double _edgeFadeHeight = 24.0;
|
||||||
|
static const double _scrollDownSize = 46.0;
|
||||||
|
static const double _materialIconSlot = 48.0;
|
||||||
static const double _unreadSeparatorHeight = 30.0;
|
static const double _unreadSeparatorHeight = 30.0;
|
||||||
static const double _unreadSeparatorInset = 72.0;
|
static const double _unreadSeparatorInset = 72.0;
|
||||||
static const double _unreadAnchorFallbackAlignment = 0.3;
|
static const double _unreadAnchorFallbackAlignment = 0.3;
|
||||||
@@ -610,6 +613,18 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
bool get _composerUnderlap =>
|
bool get _composerUnderlap =>
|
||||||
AppChatChrome.current.value != ChatChromeStyle.color || _composerFrosted;
|
AppChatChrome.current.value != ChatChromeStyle.color || _composerFrosted;
|
||||||
|
|
||||||
|
bool get _materialComposer =>
|
||||||
|
!ComposerChrome.isGlossy(AppComposerStyle.current.value);
|
||||||
|
|
||||||
|
bool get _composerPaintsSurface {
|
||||||
|
if (!_commentsMode &&
|
||||||
|
widget.chatType == 'CHANNEL' &&
|
||||||
|
_pendingForwards.value.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return _materialComposer && !_composerFrosted;
|
||||||
|
}
|
||||||
|
|
||||||
bool get _liquidChrome =>
|
bool get _liquidChrome =>
|
||||||
AppVisualStyle.current.value.glossyChrome &&
|
AppVisualStyle.current.value.glossyChrome &&
|
||||||
ChatChromeMaterial.isLiquid(AppChatChrome.current.value);
|
ChatChromeMaterial.isLiquid(AppChatChrome.current.value);
|
||||||
@@ -2679,6 +2694,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
bottomSafe: _stickers.anim.value == 0,
|
bottomSafe: _stickers.anim.value == 0,
|
||||||
chatType: _commentsMode ? 'CHAT' : widget.chatType,
|
chatType: _commentsMode ? 'CHAT' : widget.chatType,
|
||||||
chrome: _effectiveChrome,
|
chrome: _effectiveChrome,
|
||||||
|
vignette: _chromeVignette,
|
||||||
style: AppComposerStyle.current.value,
|
style: AppComposerStyle.current.value,
|
||||||
background: AppComposerBackground.current.value,
|
background: AppComposerBackground.current.value,
|
||||||
backdropKey: _pillBackdrop,
|
backdropKey: _pillBackdrop,
|
||||||
@@ -3116,6 +3132,11 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
? ui.lerpDouble(_glossyHeaderHeight, _glossySearchHeight, searchT)!
|
? ui.lerpDouble(_glossyHeaderHeight, _glossySearchHeight, searchT)!
|
||||||
: kToolbarHeight;
|
: kToolbarHeight;
|
||||||
final chrome = _effectiveChrome;
|
final chrome = _effectiveChrome;
|
||||||
|
final barExtent = MediaQuery.paddingOf(context).top + height;
|
||||||
|
final fadeStop = ((barExtent - _edgeFadeHeight) / barExtent).clamp(
|
||||||
|
0.0,
|
||||||
|
1.0,
|
||||||
|
);
|
||||||
return AppBar(
|
return AppBar(
|
||||||
backgroundColor: chrome == ChatChromeStyle.color
|
backgroundColor: chrome == ChatChromeStyle.color
|
||||||
? (glossy ? Colors.transparent : cs.surfaceContainerHigh)
|
? (glossy ? Colors.transparent : cs.surfaceContainerHigh)
|
||||||
@@ -3139,7 +3160,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
cs.surface,
|
cs.surface,
|
||||||
cs.surface.withValues(alpha: 0.0),
|
cs.surface.withValues(alpha: 0.0),
|
||||||
],
|
],
|
||||||
stops: const [0.0, 0.72, 1.0],
|
stops: [0.0, fadeStop, 1.0],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: const SizedBox.expand(),
|
child: const SizedBox.expand(),
|
||||||
@@ -5494,20 +5515,28 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
senderAvatar: _searchSenderAvatar,
|
senderAvatar: _searchSenderAvatar,
|
||||||
),
|
),
|
||||||
if (vignette) ...[
|
if (vignette) ...[
|
||||||
Positioned(
|
if (AppVisualStyle.current.value.glossyChrome)
|
||||||
top: 0,
|
Positioned(
|
||||||
left: 0,
|
top: 0,
|
||||||
right: 0,
|
|
||||||
child: _buildEdgeVignette(cs, top: true),
|
|
||||||
),
|
|
||||||
ValueListenableBuilder<double>(
|
|
||||||
valueListenable: _composerHeight,
|
|
||||||
builder: (context, height, _) => Positioned(
|
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
child: _buildEdgeVignette(cs, top: true),
|
||||||
child: _buildEdgeVignette(cs, top: false, height: height),
|
|
||||||
),
|
),
|
||||||
|
ValueListenableBuilder<double>(
|
||||||
|
valueListenable: _composerHeight,
|
||||||
|
builder: (context, height, _) => _composerPaintsSurface
|
||||||
|
? Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: height,
|
||||||
|
child: _buildEdgeFade(cs),
|
||||||
|
)
|
||||||
|
: Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: _buildEdgeVignette(cs, top: false, height: height),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
Positioned(
|
Positioned(
|
||||||
@@ -5554,6 +5583,21 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildEdgeFade(ColorScheme cs) {
|
||||||
|
return IgnorePointer(
|
||||||
|
child: Container(
|
||||||
|
height: _edgeFadeHeight,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.bottomCenter,
|
||||||
|
end: Alignment.topCenter,
|
||||||
|
colors: [cs.surface, cs.surface.withValues(alpha: 0.0)],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildEdgeVignette(
|
Widget _buildEdgeVignette(
|
||||||
ColorScheme cs, {
|
ColorScheme cs, {
|
||||||
required bool top,
|
required bool top,
|
||||||
@@ -5956,7 +6000,9 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
return ValueListenableBuilder<double>(
|
return ValueListenableBuilder<double>(
|
||||||
valueListenable: _composerHeight,
|
valueListenable: _composerHeight,
|
||||||
builder: (context, height, child) => Positioned(
|
builder: (context, height, child) => Positioned(
|
||||||
right: 16,
|
right: _materialComposer
|
||||||
|
? (_materialIconSlot - _scrollDownSize) / 2
|
||||||
|
: 16,
|
||||||
bottom: (_composerUnderlap ? height : 0) + 12,
|
bottom: (_composerUnderlap ? height : 0) + 12,
|
||||||
child: child!,
|
child: child!,
|
||||||
),
|
),
|
||||||
@@ -5971,8 +6017,8 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
child: Transform.scale(
|
child: Transform.scale(
|
||||||
scale: 0.82 + 0.18 * t,
|
scale: 0.82 + 0.18 * t,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 46,
|
width: _scrollDownSize,
|
||||||
height: 46,
|
height: _scrollDownSize,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
|
||||||
|
import 'package:komet/backend/modules/messages.dart';
|
||||||
|
import 'package:komet/core/config/app_chat_chrome.dart';
|
||||||
|
import 'package:komet/core/config/app_composer_background.dart';
|
||||||
|
import 'package:komet/core/config/app_composer_style.dart';
|
||||||
|
import 'package:komet/frontend/screens/chats/chat/upload_status.dart';
|
||||||
|
import 'package:komet/frontend/screens/chats/chat/view/chat_header.dart';
|
||||||
|
import 'package:komet/frontend/screens/chats/chat/video_note_controller.dart';
|
||||||
|
import 'package:komet/frontend/screens/chats/chat/view/composer_input.dart';
|
||||||
|
import 'package:komet/frontend/screens/chats/chat/voice_record_controller.dart';
|
||||||
|
import 'package:komet/frontend/widgets/composer_morph_icon.dart';
|
||||||
|
import 'package:komet/frontend/widgets/rich_message_controller.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late RichMessageController messageController;
|
||||||
|
late FocusNode focusNode;
|
||||||
|
late AnimationController attachAnim;
|
||||||
|
late VoiceRecordController voiceRec;
|
||||||
|
late VideoNoteController note;
|
||||||
|
late ValueNotifier<CachedMessage?> replyTo;
|
||||||
|
late ValueNotifier<List<CachedMessage>> forwards;
|
||||||
|
late ValueNotifier<bool> hasText;
|
||||||
|
late ValueNotifier<UploadStatus> uploadStatus;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
messageController = RichMessageController();
|
||||||
|
focusNode = FocusNode();
|
||||||
|
attachAnim = AnimationController(
|
||||||
|
vsync: const TestVSync(),
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
);
|
||||||
|
voiceRec = VoiceRecordController(
|
||||||
|
contextOf: () => throw UnimplementedError(),
|
||||||
|
isMounted: () => true,
|
||||||
|
myId: () => 1,
|
||||||
|
onRecorded: (File file, int durationMs, List<double> amps) async {},
|
||||||
|
);
|
||||||
|
note = VideoNoteController(
|
||||||
|
contextOf: () => throw UnimplementedError(),
|
||||||
|
isMounted: () => true,
|
||||||
|
onRecorded: (File file, int durationMs) async {},
|
||||||
|
formatElapsed: (ms) => '0:00',
|
||||||
|
bottomInset: () => 0,
|
||||||
|
);
|
||||||
|
replyTo = ValueNotifier(null);
|
||||||
|
forwards = ValueNotifier(const []);
|
||||||
|
hasText = ValueNotifier(false);
|
||||||
|
uploadStatus = ValueNotifier(const UploadStatus());
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
messageController.dispose();
|
||||||
|
focusNode.dispose();
|
||||||
|
attachAnim.dispose();
|
||||||
|
replyTo.dispose();
|
||||||
|
forwards.dispose();
|
||||||
|
hasText.dispose();
|
||||||
|
uploadStatus.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<double> pumpBar(WidgetTester tester, ComposerStyle style) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: MediaQuery(
|
||||||
|
data: const MediaQueryData(),
|
||||||
|
child: Scaffold(
|
||||||
|
body: Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: ComposerInputBar(
|
||||||
|
chatType: 'DIALOG',
|
||||||
|
chrome: ChatChromeStyle.none,
|
||||||
|
vignette: true,
|
||||||
|
style: style,
|
||||||
|
background: ComposerBackground.standard,
|
||||||
|
attachAnim: attachAnim,
|
||||||
|
replyTo: replyTo,
|
||||||
|
forwardMessages: forwards,
|
||||||
|
myId: 1,
|
||||||
|
hasText: hasText,
|
||||||
|
uploadStatus: uploadStatus,
|
||||||
|
messageController: messageController,
|
||||||
|
messageFocusNode: focusNode,
|
||||||
|
voiceRec: voiceRec,
|
||||||
|
note: note,
|
||||||
|
onToggleStickerPanel: () {},
|
||||||
|
onSendText: () {},
|
||||||
|
onScheduleMessage: () {},
|
||||||
|
onOpenAttach: () {},
|
||||||
|
onOpenAttachScheduled: () {},
|
||||||
|
onSendHistory: (entry) async {},
|
||||||
|
onCancelReply: () {},
|
||||||
|
onCancelForward: () {},
|
||||||
|
formatElapsed: (ms) => '0:00',
|
||||||
|
contextMenuBuilder: (context, state) => const SizedBox.shrink(),
|
||||||
|
isMuted: false,
|
||||||
|
onToggleMute: () {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
return tester.getSize(find.byType(ComposerInputBar)).height;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> pumpHeader(WidgetTester tester) async {
|
||||||
|
final status = ValueNotifier<String>('online');
|
||||||
|
final scheduled = ValueNotifier<int>(0);
|
||||||
|
final unread = ValueNotifier<int>(0);
|
||||||
|
addTearDown(status.dispose);
|
||||||
|
addTearDown(scheduled.dispose);
|
||||||
|
addTearDown(unread.dispose);
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Builder(
|
||||||
|
builder: (context) => Scaffold(
|
||||||
|
body: SizedBox(
|
||||||
|
height: kToolbarHeight,
|
||||||
|
child: ChatHeaderRow(
|
||||||
|
glossy: false,
|
||||||
|
frosted: false,
|
||||||
|
cs: Theme.of(context).colorScheme,
|
||||||
|
embedded: false,
|
||||||
|
chatId: 0,
|
||||||
|
heroTag: 'header',
|
||||||
|
name: 'Chat',
|
||||||
|
imageUrl: '',
|
||||||
|
chatType: 'CHAT',
|
||||||
|
isOfficial: false,
|
||||||
|
myId: 1,
|
||||||
|
headerStatus: status,
|
||||||
|
scheduledCount: scheduled,
|
||||||
|
otherUnread: unread,
|
||||||
|
showCall: true,
|
||||||
|
onClose: null,
|
||||||
|
onOpenInfo: () {},
|
||||||
|
onOpenScheduled: () {},
|
||||||
|
onCall: () {},
|
||||||
|
onMenu: (_) {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('the material composer is exactly as tall as the app bar', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
expect(await pumpBar(tester, ComposerStyle.materialYou), kToolbarHeight);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('the glossy composer keeps its taller pill layout', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
expect(
|
||||||
|
await pumpBar(tester, ComposerStyle.glossy),
|
||||||
|
greaterThan(kToolbarHeight),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('material actions sit on the app bar icon columns', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await pumpHeader(tester);
|
||||||
|
final headerWidth = tester.getSize(find.byType(ChatHeaderRow)).width;
|
||||||
|
final back = tester.getCenter(find.byIcon(Symbols.arrow_back)).dx;
|
||||||
|
final call = headerWidth - tester.getCenter(find.byIcon(Symbols.call)).dx;
|
||||||
|
final menu =
|
||||||
|
headerWidth - tester.getCenter(find.byIcon(Symbols.more_vert)).dx;
|
||||||
|
|
||||||
|
await pumpBar(tester, ComposerStyle.materialYou);
|
||||||
|
final width = tester.getSize(find.byType(ComposerInputBar)).width;
|
||||||
|
|
||||||
|
expect(tester.getCenter(find.byIcon(Symbols.face)).dx, back);
|
||||||
|
expect(width - tester.getCenter(find.byIcon(Symbols.attachment)).dx, call);
|
||||||
|
expect(width - tester.getCenter(find.byType(ComposerMorphIcon)).dx, menu);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('glossy action geometry is untouched', (tester) async {
|
||||||
|
await pumpBar(tester, ComposerStyle.glossy);
|
||||||
|
final width = tester.getSize(find.byType(ComposerInputBar)).width;
|
||||||
|
|
||||||
|
expect(tester.getCenter(find.byType(ComposerMorphIcon)).dx, width - 39);
|
||||||
|
expect(tester.getCenter(find.byIcon(Symbols.face)).dx, 38);
|
||||||
|
expect(tester.getCenter(find.byIcon(Symbols.attachment)).dx, width - 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user