diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index bb8c97bee2..ad1fb7a72b 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -19,6 +19,8 @@ abstract class AppConfig { static const String pushNotificationsAppId = 'uz.uzinfocom.uchar'; static const double borderRadius = 18.0; static const double columnWidth = 360.0; + static const double imageMessagePadding = 2.5; + static const double innerWidgetRadius = borderRadius - imageMessagePadding; static const String website = 'https://uchar.uz'; static const String enablePushTutorial = diff --git a/lib/pages/chat/events/image_bubble.dart b/lib/pages/chat/events/image_bubble.dart index 9e4bb9af60..3b258b229d 100644 --- a/lib/pages/chat/events/image_bubble.dart +++ b/lib/pages/chat/events/image_bubble.dart @@ -79,179 +79,179 @@ class ImageBubble extends StatelessWidget { @override Widget build(BuildContext context) { - var borderRadius = + final borderRadius = this.borderRadius ?? BorderRadius.circular(AppConfig.borderRadius); - final imageBorderRadius = BorderRadius.only( - topLeft: Radius.circular(AppConfig.borderRadius - 2), - topRight: Radius.circular(AppConfig.borderRadius - 2), - bottomLeft: Radius.circular(AppConfig.borderRadius / 2), - bottomRight: Radius.circular(AppConfig.borderRadius / 2), - ); - final fileDescription = event.fileDescription; final textColor = this.textColor; - if (fileDescription != null) { - borderRadius = borderRadius.copyWith( - bottomLeft: Radius.zero, - bottomRight: Radius.zero, - ); - } + // if (fileDescription != null) { + // borderRadius = borderRadius.copyWith( + // bottomLeft: Radius.zero, + // bottomRight: Radius.zero, + // ); + // } final messageTime = event.originServerTs; final formattedTime = "${messageTime.hour.toString().padLeft(2, '0')}:${messageTime.minute.toString().padLeft(2, '0')}"; - return Column( - mainAxisSize: .min, - spacing: 6, - children: [ - Material( - color: Colors.transparent, - clipBehavior: Clip.hardEdge, - shape: RoundedRectangleBorder(borderRadius: borderRadius), - child: InkWell( - onTap: () => _onTap(context), - borderRadius: borderRadius, - child: Padding( - padding: const EdgeInsets.all(2.5), - child: Hero( - tag: event.eventId, - child: ClipRRect( - borderRadius: imageBorderRadius, - child: Stack( - alignment: Alignment.bottomRight, - children: [ - MxcImage( - event: event, - width: width, - height: height, - fit: fit, - animated: animated, - isThumbnail: thumbnailOnly, - placeholder: event.messageType == MessageTypes.Sticker - ? null - : _buildPlaceholder, - ), - - if (fileDescription == null) - Padding( - padding: const EdgeInsets.only( - right: 8.0, - bottom: 8.0, + return SizedBox( + width: width, + child: Column( + mainAxisSize: .min, + spacing: 6, + children: [ + SizedBox( + height: height, + width: width, + child: Material( + color: Colors.transparent, + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder(borderRadius: borderRadius), + child: InkWell( + onTap: () => _onTap(context), + borderRadius: borderRadius, + child: Padding( + padding: const EdgeInsets.all(AppConfig.imageMessagePadding), + child: Hero( + tag: event.eventId, + child: ClipRRect( + borderRadius: borderRadius, + child: Stack( + alignment: Alignment.bottomRight, + children: [ + MxcImage( + event: event, + width: width, + height: height, + fit: fit, + animated: animated, + isThumbnail: thumbnailOnly, + placeholder: event.messageType == MessageTypes.Sticker + ? null + : _buildPlaceholder, ), - child: DecoratedBox( - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: .3), - borderRadius: BorderRadius.all( - Radius.circular(AppConfig.borderRadius / 3), + + if (fileDescription == null) + Padding( + padding: const EdgeInsets.only( + right: 8.0, + bottom: 8.0, ), - ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 7, - vertical: 3, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - formattedTime, - style: TextStyle( - color: Colors.white, - fontSize: 10, - ), + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: .3), + borderRadius: BorderRadius.all( + Radius.circular(AppConfig.borderRadius / 3), ), - - if (messageStatus != null) SizedBox(width: 6,), - - if (messageStatus != null) MessageStatusWidget( - iconColor: Colors.white, - status: messageStatus, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 7, + vertical: 3, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + formattedTime, + style: TextStyle( + color: Colors.white, + fontSize: 10, + ), + ), + + if (messageStatus != null) SizedBox(width: 6,), + + if (messageStatus != null) MessageStatusWidget( + iconColor: Colors.white, + status: messageStatus, + ), + ], ), - ], + ), ), ), - ), - ), - ], + ], + ), + ), ), ), ), ), ), - ), - if (fileDescription != null && textColor != null) - SizedBox( - width: width, - child: Padding( - padding: const EdgeInsets.only(left: 6, right: 12,), - child: SizedBox( - width: double.infinity, - child: Wrap( - // alignment: WrapAlignment.end, - alignment: WrapAlignment.start, - children: [ - Linkify( - text: fileDescription, - textScaleFactor: MediaQuery.textScalerOf( - context, - ).scale(1), - style: TextStyle( - color: textColor, - fontSize: - AppSettings.fontSizeFactor.value * - AppConfig.messageFontSize, - ), - options: const LinkifyOptions(humanize: false), - linkStyle: TextStyle( - color: linkColor, - fontSize: - AppSettings.fontSizeFactor.value * - AppConfig.messageFontSize, - decoration: TextDecoration.underline, - decorationColor: linkColor, + if (fileDescription != null && textColor != null) + SizedBox( + width: width, + child: Padding( + padding: const EdgeInsets.only(left: 6, right: 12,), + child: SizedBox( + width: double.infinity, + child: Wrap( + // alignment: WrapAlignment.end, + alignment: WrapAlignment.start, + children: [ + Linkify( + text: fileDescription, + textScaleFactor: MediaQuery.textScalerOf( + context, + ).scale(1), + style: TextStyle( + color: textColor, + fontSize: + AppSettings.fontSizeFactor.value * + AppConfig.messageFontSize, + ), + options: const LinkifyOptions(humanize: false), + linkStyle: TextStyle( + color: linkColor, + fontSize: + AppSettings.fontSizeFactor.value * + AppConfig.messageFontSize, + decoration: TextDecoration.underline, + decorationColor: linkColor, + ), + onOpen: (url) => + UrlLauncher(context, url.url).launchUrl(), ), - onOpen: (url) => - UrlLauncher(context, url.url).launchUrl(), - ), - - Align( - alignment: Alignment.topRight, - child: Transform.translate( - offset: const Offset(0, -15), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - formattedTime, - style: TextStyle( - color: textColor, - fontSize: - AppSettings.fontSizeFactor.value * - (AppConfig.messageFontSize - 5), + + Align( + alignment: Alignment.topRight, + child: Transform.translate( + offset: const Offset(0, -15), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + formattedTime, + style: TextStyle( + color: textColor, + fontSize: + AppSettings.fontSizeFactor.value * + (AppConfig.messageFontSize - 5), + ), ), - ), - - const SizedBox( - width: 6, - ), - - MessageStatusWidget( - iconColor: textColor, - status: messageStatus, - ), - ], + + const SizedBox( + width: 6, + ), + + MessageStatusWidget( + iconColor: textColor, + status: messageStatus, + ), + ], + ), ), ), - ), - ], + ], + ), ), ), ), - ), - ], + ], + ), ); } } \ No newline at end of file diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index c3bb67be43..16984d823b 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -98,8 +98,7 @@ class Message extends StatelessWidget { return StateMessage(event, onExpand: onExpand, isCollapsed: isCollapsed); } - if (event.type == EventTypes.Message && - event.messageType == EventTypes.KeyVerificationRequest) { + if (event.type == EventTypes.Message && event.messageType == EventTypes.KeyVerificationRequest) { return StateMessage(event); } @@ -114,27 +113,17 @@ class Message extends StatelessWidget { !event.originServerTs.sameEnvironment(nextEvent!.originServerTs); final nextEventSameSender = nextEvent != null && - { - EventTypes.Message, - EventTypes.Sticker, - EventTypes.Encrypted, - }.contains(nextEvent!.type) && + {EventTypes.Message, EventTypes.Sticker, EventTypes.Encrypted}.contains(nextEvent!.type) && nextEvent!.senderId == event.senderId && !displayTime; final previousEventSameSender = previousEvent != null && - { - EventTypes.Message, - EventTypes.Sticker, - EventTypes.Encrypted, - }.contains(previousEvent!.type) && + {EventTypes.Message, EventTypes.Sticker, EventTypes.Encrypted}.contains(previousEvent!.type) && previousEvent!.senderId == event.senderId && previousEvent!.originServerTs.sameEnvironment(event.originServerTs); - final textColor = ownMessage - ? theme.onBubbleColor - : theme.colorScheme.onSurface; + final textColor = ownMessage ? theme.onBubbleColor : theme.colorScheme.onSurface; final linkColor = ownMessage ? theme.brightness == Brightness.light @@ -142,9 +131,7 @@ class Message extends StatelessWidget { : theme.colorScheme.onTertiaryContainer : theme.colorScheme.primary; - final rowMainAxisAlignment = ownMessage - ? MainAxisAlignment.end - : MainAxisAlignment.start; + final rowMainAxisAlignment = ownMessage ? MainAxisAlignment.end : MainAxisAlignment.start; final displayEvent = event.getDisplayEvent(timeline); const hardCorner = Radius.circular(4); @@ -152,19 +139,21 @@ class Message extends StatelessWidget { final borderRadius = BorderRadius.only( topLeft: !ownMessage && nextEventSameSender ? hardCorner : roundedCorner, topRight: ownMessage && nextEventSameSender ? hardCorner : roundedCorner, - bottomLeft: !ownMessage && previousEventSameSender - ? hardCorner - : roundedCorner, - bottomRight: ownMessage && previousEventSameSender - ? hardCorner - : roundedCorner, + bottomLeft: !ownMessage && previousEventSameSender ? hardCorner : roundedCorner, + bottomRight: ownMessage && previousEventSameSender ? hardCorner : roundedCorner, ); + + final innerRoundedCorner = Radius.circular(AppConfig.innerWidgetRadius); + + final innerWidgetRadius = BorderRadius.only( + topLeft: !ownMessage && nextEventSameSender ? hardCorner : innerRoundedCorner, + topRight: ownMessage && nextEventSameSender ? hardCorner : innerRoundedCorner, + bottomLeft: !ownMessage && previousEventSameSender ? hardCorner : innerRoundedCorner, + bottomRight: ownMessage && previousEventSameSender ? hardCorner : innerRoundedCorner, + ); + final noBubble = - ({ - MessageTypes.Video, - MessageTypes.Image, - MessageTypes.Sticker, - }.contains(event.messageType) && + ({MessageTypes.Video, MessageTypes.Sticker}.contains(event.messageType) && event.fileDescription == null && !event.redacted) || (event.messageType == MessageTypes.Text && @@ -174,9 +163,7 @@ class Message extends StatelessWidget { event.numberEmotes <= 3); if (ownMessage) { - color = displayEvent.status.isError - ? Colors.redAccent - : theme.bubbleColor; + color = displayEvent.status.isError ? Colors.redAccent : theme.bubbleColor; } final resetAnimateIn = this.resetAnimateIn; @@ -187,32 +174,17 @@ class Message extends StatelessWidget { sentReactions.addAll( event .aggregatedEvents(timeline, RelationshipTypes.reaction) - .where( - (event) => - event.senderId == event.room.client.userID && - event.type == 'm.reaction', - ) - .map( - (event) => event.content - .tryGetMap('m.relates_to') - ?.tryGet('key'), - ) + .where((event) => event.senderId == event.room.client.userID && event.type == 'm.reaction') + .map((event) => event.content.tryGetMap('m.relates_to')?.tryGet('key')) .whereType(), ); } - final showReceiptsRow = event.hasAggregatedEvents( - timeline, - RelationshipTypes.reaction, - ); + final showReceiptsRow = event.hasAggregatedEvents(timeline, RelationshipTypes.reaction); - final threadChildren = event.aggregatedEvents( - timeline, - RelationshipTypes.thread, - ); + final threadChildren = event.aggregatedEvents(timeline, RelationshipTypes.thread); - final showReactionPicker = - singleSelected && event.room.canSendDefaultMessages; + final showReactionPicker = singleSelected && event.room.canSendDefaultMessages; final enterThread = this.enterThread; @@ -224,9 +196,7 @@ class Message extends StatelessWidget { onSwipe(); }, child: Container( - constraints: const BoxConstraints( - maxWidth: FluffyThemes.maxTimelineWidth, - ), + constraints: const BoxConstraints(maxWidth: FluffyThemes.maxTimelineWidth), padding: EdgeInsets.only( left: 8.0, right: 8.0, @@ -239,22 +209,15 @@ class Message extends StatelessWidget { children: [ if (displayTime || selected) Padding( - padding: displayTime - ? const EdgeInsets.symmetric(vertical: 8.0) - : EdgeInsets.zero, + padding: displayTime ? const EdgeInsets.symmetric(vertical: 8.0) : EdgeInsets.zero, child: Center( child: Padding( padding: const EdgeInsets.only(top: 4.0), child: Material( - borderRadius: BorderRadius.circular( - AppConfig.borderRadius * 2, - ), + borderRadius: BorderRadius.circular(AppConfig.borderRadius * 2), color: theme.colorScheme.surface.withAlpha(128), child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8.0, - vertical: 2.0, - ), + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 2.0), child: Text( event.originServerTs.localizedTime(context), style: TextStyle( @@ -268,6 +231,7 @@ class Message extends StatelessWidget { ), ), ), + StatefulBuilder( builder: (context, setState) { if (animateIn && resetAnimateIn != null) { @@ -280,45 +244,37 @@ class Message extends StatelessWidget { duration: FluffyThemes.animationDuration, curve: FluffyThemes.animationCurve, clipBehavior: Clip.none, - alignment: ownMessage - ? Alignment.bottomRight - : Alignment.bottomLeft, + alignment: ownMessage ? Alignment.bottomRight : Alignment.bottomLeft, child: animateIn ? const SizedBox(height: 0, width: double.infinity) : Stack( clipBehavior: Clip.none, children: [ + // select background Positioned( top: 0, bottom: 0, left: 0, right: 0, child: InkWell( - hoverColor: longPressSelect - ? Colors.transparent - : null, + hoverColor: longPressSelect ? Colors.transparent : null, enableFeedback: !selected, - onTap: longPressSelect - ? null - : () => onSelect(event), - borderRadius: BorderRadius.circular( - AppConfig.borderRadius / 2, - ), + onTap: longPressSelect ? null : () => onSelect(event), + borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2), child: Material( - borderRadius: BorderRadius.circular( - AppConfig.borderRadius / 2, - ), + borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2), color: selected || highlightMarker - ? theme.colorScheme.secondaryContainer - .withAlpha(128) + ? theme.colorScheme.secondaryContainer.withAlpha(128) : Colors.transparent, ), ), ), + Row( crossAxisAlignment: .start, mainAxisAlignment: rowMainAxisAlignment, children: [ + // avatar if (longPressSelect && !event.redacted) SizedBox( height: 32, @@ -326,11 +282,7 @@ class Message extends StatelessWidget { child: IconButton( padding: EdgeInsets.zero, tooltip: L10n.of(context).select, - icon: Icon( - selected - ? Icons.check_circle - : Icons.circle_outlined, - ), + icon: Icon(selected ? Icons.check_circle : Icons.circle_outlined), onPressed: () => onSelect(event), ), ) @@ -341,16 +293,10 @@ class Message extends StatelessWidget { child: SizedBox( width: 16, height: 16, - child: - event.status == EventStatus.error - ? const Icon( - Icons.error, - color: Colors.red, - ) + child: event.status == EventStatus.error + ? const Icon(Icons.error, color: Colors.red) : event.fileSendingStatus != null - ? const CircularProgressIndicator.adaptive( - strokeWidth: 1, - ) + ? const CircularProgressIndicator.adaptive(strokeWidth: 1) : null, ), ), @@ -359,93 +305,66 @@ class Message extends StatelessWidget { FutureBuilder( future: event.fetchSenderUser(), builder: (context, snapshot) { - final user = - snapshot.data ?? - event.senderFromMemoryOrFallback; + final user = snapshot.data ?? event.senderFromMemoryOrFallback; return Avatar( mxContent: user.avatarUrl, name: user.calcDisplayname(), - onTap: () => - showMemberActionsPopupMenu( - context: context, - user: user, - onMention: onMention, - ), + onTap: () => showMemberActionsPopupMenu( + context: context, + user: user, + onMention: onMention, + ), presenceUserId: user.stateKey, - presenceBackgroundColor: wallpaperMode - ? Colors.transparent - : null, + presenceBackgroundColor: wallpaperMode ? Colors.transparent : null, ); }, ), + Expanded( child: Column( crossAxisAlignment: .start, mainAxisSize: .min, children: [ + // date if (!nextEventSameSender) Padding( - padding: const EdgeInsets.only( - left: 8.0, - bottom: 4, - ), - child: - ownMessage || - event.room.isDirectChat + padding: const EdgeInsets.only(left: 8.0, bottom: 4), + child: ownMessage || event.room.isDirectChat ? const SizedBox(height: 12) : FutureBuilder( - future: event - .fetchSenderUser(), + future: event.fetchSenderUser(), builder: (context, snapshot) { final displayname = - snapshot.data - ?.calcDisplayname() ?? - event - .senderFromMemoryOrFallback - .calcDisplayname(); + snapshot.data?.calcDisplayname() ?? + event.senderFromMemoryOrFallback.calcDisplayname(); return Text( displayname, style: TextStyle( fontSize: 11, - fontWeight: - FontWeight.bold, - color: - (theme.brightness == - Brightness - .light - ? displayname - .color - : displayname - .lightColorText), - shadows: - !wallpaperMode + fontWeight: FontWeight.bold, + color: (theme.brightness == Brightness.light + ? displayname.color + : displayname.lightColorText), + shadows: !wallpaperMode ? null : [ const Shadow( - offset: - Offset( - 0.0, - 0.0, - ), - blurRadius: - 3, - color: Colors - .black, + offset: Offset(0.0, 0.0), + blurRadius: 3, + color: Colors.black, ), ], ), maxLines: 1, - overflow: TextOverflow - .ellipsis, + overflow: TextOverflow.ellipsis, ); }, ), ), + Container( alignment: alignment, - padding: const EdgeInsets.only( - left: 8, - ), + padding: const EdgeInsets.only(left: 8), child: GestureDetector( onLongPress: longPressSelect ? null @@ -456,206 +375,118 @@ class Message extends StatelessWidget { child: AnimatedOpacity( opacity: animateIn ? 0 - : event.messageType == - MessageTypes - .BadEncrypted || + : event.messageType == MessageTypes.BadEncrypted || event.status.isSending ? 0.5 : 1, - duration: FluffyThemes - .animationDuration, - curve: - FluffyThemes.animationCurve, + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, child: Container( decoration: BoxDecoration( - color: noBubble - ? Colors.transparent - : color, + color: noBubble ? Colors.transparent : color, borderRadius: borderRadius, ), clipBehavior: Clip.antiAlias, child: BubbleBackground( colors: colors, - ignore: - noBubble || - !ownMessage || - MediaQuery.highContrastOf( - context, - ), - scrollController: - scrollController, + ignore: noBubble || !ownMessage || MediaQuery.highContrastOf(context), + scrollController: scrollController, child: Container( decoration: BoxDecoration( - borderRadius: - BorderRadius.circular( - AppConfig - .borderRadius, - ), + borderRadius: BorderRadius.circular(AppConfig.borderRadius), + ), + constraints: const BoxConstraints( + maxWidth: FluffyThemes.columnWidth * 1.5, ), - constraints: - const BoxConstraints( - maxWidth: - FluffyThemes - .columnWidth * - 1.5, - ), child: Column( mainAxisSize: .min, - crossAxisAlignment: - CrossAxisAlignment - .start, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (event.inReplyToEventId( - includingFallback: - false, - ) != - null) + // replying content + if (event.inReplyToEventId(includingFallback: false) != null) FutureBuilder( - future: event - .getReplyEvent( - timeline, + future: event.getReplyEvent(timeline), + builder: (BuildContext context, snapshot) { + final replyEvent = snapshot.hasData + ? snapshot.data! + : Event( + eventId: + event.inReplyToEventId() ?? '\$fake_event_id', + content: {'msgtype': 'm.text', 'body': '...'}, + senderId: event.senderId, + type: 'm.room.message', + room: event.room, + status: EventStatus.sent, + originServerTs: DateTime.now(), + ); + return Padding( + padding: const EdgeInsets.only( + left: 16, + right: 16, + top: 8, ), - builder: - ( - BuildContext - context, - snapshot, - ) { - final replyEvent = - snapshot - .hasData - ? snapshot - .data! - : Event( - eventId: - event.inReplyToEventId() ?? - '\$fake_event_id', - content: { - 'msgtype': - 'm.text', - 'body': - '...', - }, - senderId: - event.senderId, - type: - 'm.room.message', - room: - event.room, - status: - EventStatus.sent, - originServerTs: - DateTime.now(), - ); - return Padding( - padding: - const EdgeInsets.only( - left: - 16, - right: - 16, - top: - 8, - ), - child: Material( - color: Colors - .transparent, - borderRadius: - ReplyContent - .borderRadius, - child: InkWell( - borderRadius: - ReplyContent.borderRadius, - onTap: () => scrollToEventId( - replyEvent - .eventId, - ), - child: AbsorbPointer( - child: ReplyContent( - replyEvent, - ownMessage: - ownMessage, - timeline: - timeline, - ), - ), + child: Material( + color: Colors.transparent, + borderRadius: ReplyContent.borderRadius, + child: InkWell( + borderRadius: ReplyContent.borderRadius, + onTap: () => scrollToEventId(replyEvent.eventId), + child: AbsorbPointer( + child: ReplyContent( + replyEvent, + ownMessage: ownMessage, + timeline: timeline, ), ), - ); - }, + ), + ), + ); + }, ), + Builder( builder: (context) { - MessageStatus? - myMessageStatus; + MessageStatus? myMessageStatus; // debugPrint("message status: ${displayEvent.status}, ${displayEvent.content}"); - switch (displayEvent - .status) { - case EventStatus - .sending: + switch (displayEvent.status) { + case EventStatus.sending: { - myMessageStatus = - MessageStatus - .pending; + myMessageStatus = MessageStatus.pending; break; } - case EventStatus - .error: + case EventStatus.error: { - myMessageStatus = - MessageStatus - .error; + myMessageStatus = MessageStatus.error; break; } - case EventStatus - .sent: - case EventStatus - .synced: + case EventStatus.sent: + case EventStatus.synced: { // Find latest read position across all participants - final latestReadEventId = - getLatestReadEventId( - timeline, - client.userID ?? - '', - ); + final latestReadEventId = getLatestReadEventId( + timeline, + client.userID ?? '', + ); - if (latestReadEventId == - null) { + if (latestReadEventId == null) { // No one has read anything - myMessageStatus = - MessageStatus - .sent; + myMessageStatus = MessageStatus.sent; } else { // Compare positions: find indices in timeline - final latestReadIndex = timeline - .events - .indexWhere( - (e) => - e.eventId == - latestReadEventId, - ); - final currentMessageIndex = timeline - .events - .indexWhere( - (e) => - e.eventId == - event.eventId, - ); + final latestReadIndex = timeline.events.indexWhere( + (e) => e.eventId == latestReadEventId, + ); + final currentMessageIndex = timeline.events + .indexWhere((e) => e.eventId == event.eventId); // Lower index = newer, higher index = older // If current message index >= latest read index, it's been read - if (currentMessageIndex >= - latestReadIndex) { - myMessageStatus = - MessageStatus - .seen; + if (currentMessageIndex >= latestReadIndex) { + myMessageStatus = MessageStatus.seen; } else { - myMessageStatus = - MessageStatus - .sent; + myMessageStatus = MessageStatus.sent; } } @@ -664,70 +495,45 @@ class Message extends StatelessWidget { } if (!ownMessage) { - myMessageStatus = - null; + myMessageStatus = null; } return MessageContent( displayEvent, - textColor: - textColor, - linkColor: - linkColor, - onInfoTab: - onInfoTab, - borderRadius: - borderRadius, - timeline: - timeline, - selected: - selected, - messageStatus: - myMessageStatus, + textColor: textColor, + linkColor: linkColor, + onInfoTab: onInfoTab, + borderRadius: borderRadius, + innerBorderRadius: innerWidgetRadius, + timeline: timeline, + selected: selected, + messageStatus: myMessageStatus, ); }, ), - if (event - .hasAggregatedEvents( - timeline, - RelationshipTypes - .edit, - )) + if (event.hasAggregatedEvents(timeline, RelationshipTypes.edit)) Padding( - padding: - const EdgeInsets.only( - bottom: 8.0, - left: 16.0, - right: 16.0, - ), + padding: const EdgeInsets.only( + bottom: 8.0, + left: 16.0, + right: 16.0, + ), child: Row( - mainAxisSize: - MainAxisSize - .min, + mainAxisSize: MainAxisSize.min, spacing: 4.0, children: [ Icon( - Icons - .edit_outlined, - color: textColor - .withAlpha( - 164, - ), + Icons.edit_outlined, + color: textColor.withAlpha(164), size: 14, ), Text( - displayEvent - .originServerTs - .localizedTimeShort( - context, - ), + displayEvent.originServerTs.localizedTimeShort( + context, + ), style: TextStyle( - color: textColor - .withAlpha( - 164, - ), - fontSize: - 11, + color: textColor.withAlpha(164), + fontSize: 11, ), ), ], @@ -742,146 +548,80 @@ class Message extends StatelessWidget { ), ), Align( - alignment: ownMessage - ? Alignment.bottomRight - : Alignment.bottomLeft, + alignment: ownMessage ? Alignment.bottomRight : Alignment.bottomLeft, child: AnimatedSize( - duration: - FluffyThemes.animationDuration, + duration: FluffyThemes.animationDuration, curve: FluffyThemes.animationCurve, child: showReactionPicker ? Padding( - padding: - const EdgeInsets.all( - 4.0, - ), + padding: const EdgeInsets.all(4.0), child: Material( elevation: 4, - borderRadius: - BorderRadius.circular( - AppConfig - .borderRadius, - ), - shadowColor: theme - .colorScheme - .surface - .withAlpha(128), + borderRadius: BorderRadius.circular(AppConfig.borderRadius), + shadowColor: theme.colorScheme.surface.withAlpha(128), child: SingleChildScrollView( - scrollDirection: - Axis.horizontal, + scrollDirection: Axis.horizontal, child: Row( mainAxisSize: .min, children: [ ...AppConfig.defaultReactions.map( - ( - emoji, - ) => IconButton( - padding: - EdgeInsets - .zero, + (emoji) => IconButton( + padding: EdgeInsets.zero, icon: Center( child: Opacity( - opacity: - sentReactions.contains( - emoji, - ) - ? 0.33 - : 1, + opacity: sentReactions.contains(emoji) ? 0.33 : 1, child: Text( emoji, - style: const TextStyle( - fontSize: - 20, - ), - textAlign: - TextAlign - .center, + style: const TextStyle(fontSize: 20), + textAlign: TextAlign.center, ), ), ), - onPressed: - sentReactions - .contains( - emoji, - ) + onPressed: sentReactions.contains(emoji) ? null : () { - onSelect( - event, - ); - event.room.sendReaction( - event - .eventId, - emoji, - ); + onSelect(event); + event.room.sendReaction(event.eventId, emoji); }, ), ), IconButton( - icon: const Icon( - Icons - .add_reaction_outlined, - ), - tooltip: L10n.of( - context, - ).customReaction, + icon: const Icon(Icons.add_reaction_outlined), + tooltip: L10n.of(context).customReaction, onPressed: () async { final emoji = await showAdaptiveBottomSheet( - context: - context, + context: context, builder: (context) => Scaffold( appBar: AppBar( - title: Text( - L10n.of( - context, - ).customReaction, - ), + title: Text(L10n.of(context).customReaction), leading: CloseButton( - onPressed: () => Navigator.of( - context, - ).pop(null), + onPressed: () => + Navigator.of(context).pop(null), ), ), body: SizedBox( - height: double - .infinity, + height: double.infinity, child: EmojiPicker( - onEmojiSelected: - ( - _, - emoji, - ) => - Navigator.of( - context, - ).pop( - emoji.emoji, - ), + onEmojiSelected: (_, emoji) => + Navigator.of(context).pop(emoji.emoji), config: Config( - locale: Localizations.localeOf( - context, - ), + locale: Localizations.localeOf(context), emojiViewConfig: const EmojiViewConfig( - backgroundColor: - Colors.transparent, - ), - bottomActionBarConfig: const BottomActionBarConfig( - enabled: - false, + backgroundColor: Colors.transparent, ), + bottomActionBarConfig: + const BottomActionBarConfig( + enabled: false, + ), categoryViewConfig: CategoryViewConfig( - initCategory: - Category.SMILEYS, - backspaceColor: - theme.colorScheme.primary, - iconColor: theme.colorScheme.primary.withAlpha( - 128, - ), + initCategory: Category.SMILEYS, + backspaceColor: theme.colorScheme.primary, + iconColor: theme.colorScheme.primary + .withAlpha(128), iconColorSelected: theme.colorScheme.primary, - indicatorColor: - theme.colorScheme.primary, - backgroundColor: - theme.colorScheme.surface, + indicatorColor: theme.colorScheme.primary, + backgroundColor: theme.colorScheme.surface, ), skinToneConfig: SkinToneConfig( dialogBackgroundColor: Color.lerp( @@ -889,32 +629,22 @@ class Message extends StatelessWidget { theme.colorScheme.primaryContainer, 0.75, )!, - indicatorColor: - theme.colorScheme.onSurface, + indicatorColor: theme.colorScheme.onSurface, ), ), ), ), ), ); - if (emoji == - null) { + if (emoji == null) { return; } - if (sentReactions - .contains( - emoji, - )) { + if (sentReactions.contains(emoji)) { return; } onSelect(event); - await event.room - .sendReaction( - event - .eventId, - emoji, - ); + await event.room.sendReaction(event.eventId, emoji); }, ), ], @@ -935,6 +665,8 @@ class Message extends StatelessWidget { ); }, ), + + // reacted reactions AnimatedSize( duration: FluffyThemes.animationDuration, curve: FluffyThemes.animationCurve, @@ -950,6 +682,7 @@ class Message extends StatelessWidget { child: MessageReactions(event, timeline), ), ), + if (enterThread != null) AnimatedSize( duration: FluffyThemes.animationDuration, @@ -958,24 +691,14 @@ class Message extends StatelessWidget { child: threadChildren.isEmpty ? const SizedBox.shrink() : Padding( - padding: const EdgeInsets.only( - top: 2.0, - bottom: 8.0, - left: Avatar.defaultSize + 8, - ), + padding: const EdgeInsets.only(top: 2.0, bottom: 8.0, left: Avatar.defaultSize + 8), child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: FluffyThemes.columnWidth * 1.5, - ), + constraints: const BoxConstraints(maxWidth: FluffyThemes.columnWidth * 1.5), child: TextButton.icon( style: TextButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), - ), - foregroundColor: - theme.colorScheme.onSecondaryContainer, - backgroundColor: - theme.colorScheme.secondaryContainer, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), + foregroundColor: theme.colorScheme.onSecondaryContainer, + backgroundColor: theme.colorScheme.secondaryContainer, ), onPressed: () => enterThread(event.eventId), icon: const Icon(Icons.message), @@ -988,41 +711,24 @@ class Message extends StatelessWidget { ), ), ), + if (displayReadMarker) Row( children: [ - Expanded( - child: Divider( - color: theme.colorScheme.surfaceContainerHighest, - ), - ), + Expanded(child: Divider(color: theme.colorScheme.surfaceContainerHighest)), Container( - margin: const EdgeInsets.symmetric( - horizontal: 4, - vertical: 16.0, - ), - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 2, - ), + margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 16.0), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( - borderRadius: BorderRadius.circular( - AppConfig.borderRadius / 3, - ), + borderRadius: BorderRadius.circular(AppConfig.borderRadius / 3), color: theme.colorScheme.surface.withAlpha(128), ), child: Text( L10n.of(context).readUpToHere, - style: TextStyle( - fontSize: 12 * AppSettings.fontSizeFactor.value, - ), - ), - ), - Expanded( - child: Divider( - color: theme.colorScheme.surfaceContainerHighest, + style: TextStyle(fontSize: 12 * AppSettings.fontSizeFactor.value), ), ), + Expanded(child: Divider(color: theme.colorScheme.surfaceContainerHighest)), ], ), ], @@ -1051,22 +757,14 @@ class BubbleBackground extends StatelessWidget { Widget build(BuildContext context) { if (ignore) return child; return CustomPaint( - painter: BubblePainter( - repaint: scrollController, - colors: colors, - context: context, - ), + painter: BubblePainter(repaint: scrollController, colors: colors, context: context), child: child, ); } } class BubblePainter extends CustomPainter { - BubblePainter({ - required this.context, - required this.colors, - required super.repaint, - }); + BubblePainter({required this.context, required this.colors, required super.repaint}); final BuildContext context; final List colors; @@ -1079,10 +777,7 @@ class BubblePainter extends CustomPainter { final scrollableRect = Offset.zero & scrollableBox.size; final bubbleBox = context.findRenderObject() as RenderBox; - final origin = bubbleBox.localToGlobal( - Offset.zero, - ancestor: scrollableBox, - ); + final origin = bubbleBox.localToGlobal(Offset.zero, ancestor: scrollableBox); final paint = Paint() ..shader = ui.Gradient.linear( scrollableRect.topCenter, diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index fcc2c890da..340b530ac5 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -34,6 +34,7 @@ class MessageContent extends StatelessWidget { final Color linkColor; final void Function(Event)? onInfoTab; final BorderRadius borderRadius; + final BorderRadius innerBorderRadius; final Timeline timeline; final bool selected; final MessageStatus? messageStatus; @@ -46,8 +47,10 @@ class MessageContent extends StatelessWidget { required this.textColor, required this.linkColor, required this.borderRadius, + required this.innerBorderRadius, required this.selected, required this.messageStatus, + }); void _verifyOrRequestKey(BuildContext context) async { @@ -114,47 +117,52 @@ class MessageContent extends StatelessWidget { switch (event.messageType) { case MessageTypes.Image: case MessageTypes.Sticker: - if (event.redacted) continue textmessage; - - final screenWidth = MediaQuery.of(context).size.width; + return LayoutBuilder( + builder: (context, constraints) { + final maxBubbleWidth = constraints.maxWidth * 0.7; - final targetWidth = screenWidth * 0.7; - final maxHeight = screenWidth * 0.9; + final info = event.content.tryGetMap('info'); + final w = info?.tryGet('w')?.toDouble() ?? 1.0; + final h = info?.tryGet('h')?.toDouble() ?? 1.0; - final info = event.content.tryGetMap('info'); - final w = info?.tryGet('w')?.toDouble() ?? 1.0; - final h = info?.tryGet('h')?.toDouble() ?? 1.0; + double width; + double height; + var fit = BoxFit.cover; - double width; - double height; - var fit = BoxFit.cover; + if (event.messageType == MessageTypes.Sticker) { + width = 160; + height = 160; + fit = BoxFit.contain; + } else { + final aspectRatio = w / h; - if (event.messageType == MessageTypes.Sticker) { - width = 160.0; - height = 160.0; - fit = BoxFit.contain; - } else { - width = targetWidth; + width = maxBubbleWidth; - // We calculate the height based on the proportions of the image - // But we limit it from being too small or too large - final aspectRatio = w / h; - height = (width / aspectRatio).clamp(150.0, maxHeight); - - // If the image is very vertical (e.g. a screenshot), - // we will limit the height and center the image - } + height = width / aspectRatio; - return ImageBubble( - event, - width: width, - height: height, - fit: fit, - borderRadius: borderRadius, - timeline: timeline, - textColor: textColor, - messageStatus: messageStatus, + const minHeight = 120.0; + final maxHeight = constraints.maxHeight.isFinite + ? constraints.maxHeight * 0.6 + : 420.0; + + height = height.clamp(minHeight, maxHeight); + width = height * aspectRatio; + width = width.clamp(120.0, maxBubbleWidth); + } + + return ImageBubble( + event, + width: width, + height: height, + fit: fit, + borderRadius: innerBorderRadius, + timeline: timeline, + textColor: textColor, + messageStatus: messageStatus, + ); + }, ); + case CuteEventContent.eventType: return CuteContent(event); case MessageTypes.Audio: @@ -305,6 +313,7 @@ class MessageContent extends StatelessWidget { padding: const EdgeInsets.only(top: 6, left: 8), child: Row( mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, children: [ Builder( builder: (context) { diff --git a/pubspec.lock b/pubspec.lock index 74c75fc8c1..4435bb474d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -157,10 +157,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" charcode: dependency: transitive description: @@ -1224,18 +1224,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" matrix: dependency: "direct main" description: @@ -1973,26 +1973,26 @@ packages: dependency: transitive description: name: test - sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" + sha256: "77cc98ea27006c84e71a7356cf3daf9ddbde2d91d84f77dbfe64cf0e4d9611ae" url: "https://pub.dev" source: hosted - version: "1.26.3" + version: "1.28.0" test_api: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.8" test_core: dependency: transitive description: name: test_core - sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" + sha256: f1072617a6657e5fc09662e721307f7fb009b4ed89b19f47175d11d5254a62d4 url: "https://pub.dev" source: hosted - version: "0.6.12" + version: "0.6.14" timezone: dependency: transitive description: