diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f769ec3cc..82c421d69 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -46,13 +46,25 @@ jobs: asset_path: fluffychat-web.tar.gz asset_name: fluffychat-web.tar.gz asset_content_type: application/gzip - - name: Build Website + - name: Clone fluffychat website run: | - mkdir public + git@github.com:krille-chan/fluffychat-website.git + cp CHANGELOG.md fluffychat-website/ + cp PRIVACY.md fluffychat-website/ + - name: Build website + working-directory: fluffychat-website + run: | + npm install tailwindcss @tailwindcss/cli + npx tailwindcss -i ./src/styles.css -o ./src/assets/tailwind.css --minify + npx @11ty/eleventy + mv public ../ + - name: Copy FluffyChat web into it + run: | + mkdir public/web mkdir public/nightly - cp -r build/web/* public/ + cp -r build/web/* public/web/ cp -r build/web/* public/nightly/ - echo "app.fluffy.chat" >> public/CNAME + echo "fluffychat.im" >> public/CNAME - name: Deploy to GitHub Pages if: startsWith(github.ref, 'refs/tags/v') uses: peaceiris/actions-gh-pages@v4 @@ -60,7 +72,7 @@ jobs: personal_token: ${{ secrets.PAGES_DEPLOY_TOKEN }} publish_dir: ./public publish_branch: gh-pages - cname: app.fluffy.chat + cname: fluffychat.im build_apk: runs-on: ubuntu-latest diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index afe579207..6432bc393 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -462,21 +462,18 @@ class ChatController extends State scrollUpBannerEventId = eventId; }); + bool firstUpdateReceived = false; + void updateView() { if (!mounted) return; setReadMarker(); - setState(() {}); + setState(() { + firstUpdateReceived = true; + }); } Future? loadTimelineFuture; - int? animateInEventIndex; - - void onInsert(int i) { - // setState will be called by updateView() anyway - if (timeline?.allowNewEvent == true) animateInEventIndex = i; - } - Future _getTimeline({String? eventContextId}) async { await Matrix.of(context).client.roomsLoading; await Matrix.of(context).client.accountDataLoading; @@ -489,15 +486,11 @@ class ChatController extends State timeline = await room.getTimeline( onUpdate: updateView, eventContextId: eventContextId, - onInsert: onInsert, ); } catch (e, s) { Logs().w('Unable to load timeline on event ID $eventContextId', e, s); if (!mounted) return; - timeline = await room.getTimeline( - onUpdate: updateView, - onInsert: onInsert, - ); + timeline = await room.getTimeline(onUpdate: updateView); if (!mounted) return; if (e is TimeoutException || e is IOException) { _showScrollUpMaterialBanner(eventContextId!); diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 600d0af41..79cf53547 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -35,7 +35,6 @@ class ChatEventList extends StatelessWidget { final events = timeline.events.filterByVisibleInGui( threadId: controller.activeThreadId, ); - final animateInEventIndex = controller.animateInEventIndex; // create a map of eventId --> index to greatly improve performance of // ListView's findChildIndexCallback @@ -120,10 +119,7 @@ class ChatEventList extends StatelessWidget { // The message at this index: final event = events[i]; - final animateIn = - animateInEventIndex != null && - timeline.events.length > animateInEventIndex && - event == timeline.events[animateInEventIndex]; + final animateIn = i == 0 && controller.firstUpdateReceived; final nextEvent = i + 1 < events.length ? events[i + 1] : null; final previousEvent = i > 0 ? events[i - 1] : null; @@ -139,16 +135,13 @@ class ChatEventList extends StatelessWidget { !controller.expandedEventIds.contains(event.eventId); return AutoScrollTag( - key: ValueKey(event.eventId), + key: ValueKey(event.transactionId ?? event.eventId), index: i, controller: controller.scrollController, child: Message( event, bigEmojis: controller.bigEmojis, animateIn: animateIn, - resetAnimateIn: () { - controller.animateInEventIndex = null; - }, onSwipe: () => controller.replyAction(replyTo: event), onInfoTab: controller.showEventInfo, onMention: () => controller.sendController.text += diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index a350599e6..580cdf324 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -42,7 +42,6 @@ class Message extends StatelessWidget { final Timeline timeline; final bool highlightMarker; final bool animateIn; - final void Function()? resetAnimateIn; final bool wallpaperMode; final ScrollController scrollController; final List colors; @@ -67,7 +66,6 @@ class Message extends StatelessWidget { required this.timeline, this.highlightMarker = false, this.animateIn = false, - this.resetAnimateIn, this.wallpaperMode = false, required this.onMention, required this.scrollController, @@ -171,9 +169,6 @@ class Message extends StatelessWidget { : theme.bubbleColor; } - final resetAnimateIn = this.resetAnimateIn; - var animateIn = this.animateIn; - final sentReactions = {}; if (singleSelected) { sentReactions.addAll( @@ -209,753 +204,661 @@ class Message extends StatelessWidget { final enterThread = this.enterThread; final sender = event.senderFromMemoryOrFallback; - return Center( - child: Swipeable( - key: ValueKey(event.eventId), - background: const Padding( - padding: EdgeInsets.symmetric(horizontal: 12.0), - child: Center(child: Icon(Icons.check_outlined)), - ), - direction: AppSettings.swipeRightToLeftToReply.value - ? SwipeDirection.endToStart - : SwipeDirection.startToEnd, - onSwipe: (_) => onSwipe(), - child: Container( - constraints: const BoxConstraints( - maxWidth: FluffyThemes.maxTimelineWidth, + return _AnimateIn( + animateIn: animateIn, + child: Center( + child: Swipeable( + key: ValueKey(event.eventId), + background: const Padding( + padding: EdgeInsets.symmetric(horizontal: 12.0), + child: Center(child: Icon(Icons.check_outlined)), ), - padding: EdgeInsets.only( - left: 8.0, - right: 8.0, - top: nextEventSameSender ? 1.0 : 4.0, - bottom: previousEventSameSender ? 1.0 : 4.0, - ), - child: Column( - mainAxisSize: .min, - crossAxisAlignment: ownMessage ? .end : .start, - children: [ - if (displayTime || selected) - Padding( - 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, - ), - color: theme.colorScheme.surface.withAlpha(128), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8.0, - vertical: 2.0, + direction: AppSettings.swipeRightToLeftToReply.value + ? SwipeDirection.endToStart + : SwipeDirection.startToEnd, + onSwipe: (_) => onSwipe(), + child: Container( + constraints: const BoxConstraints( + maxWidth: FluffyThemes.maxTimelineWidth, + ), + padding: EdgeInsets.only( + left: 8.0, + right: 8.0, + top: nextEventSameSender ? 1.0 : 4.0, + bottom: previousEventSameSender ? 1.0 : 4.0, + ), + child: Column( + mainAxisSize: .min, + crossAxisAlignment: ownMessage ? .end : .start, + children: [ + if (displayTime || selected) + Padding( + 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, ), - child: Text( - event.originServerTs.localizedTime(context), - style: TextStyle( - fontSize: 12 * AppSettings.fontSizeFactor.value, - fontWeight: FontWeight.bold, - color: theme.colorScheme.secondary, + color: theme.colorScheme.surface.withAlpha(128), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 2.0, + ), + child: Text( + event.originServerTs.localizedTime(context), + style: TextStyle( + fontSize: 12 * AppSettings.fontSizeFactor.value, + fontWeight: FontWeight.bold, + color: theme.colorScheme.secondary, + ), ), ), ), ), ), ), - ), - StatefulBuilder( - builder: (context, setState) { - if (animateIn && resetAnimateIn != null) { - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - animateIn = false; - setState(resetAnimateIn); - }); - } - return AnimatedSize( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - clipBehavior: Clip.none, - alignment: ownMessage - ? Alignment.bottomRight - : Alignment.bottomLeft, - child: animateIn - ? const SizedBox(height: 0, width: double.infinity) - : Stack( - clipBehavior: Clip.none, + Stack( + clipBehavior: Clip.none, + children: [ + Positioned( + top: 0, + bottom: 0, + left: 0, + right: 0, + child: InkWell( + hoverColor: longPressSelect ? Colors.transparent : null, + enableFeedback: !selected, + onTap: longPressSelect ? null : () => onSelect(event), + borderRadius: BorderRadius.circular( + AppConfig.borderRadius / 2, + ), + child: Material( + borderRadius: BorderRadius.circular( + AppConfig.borderRadius / 2, + ), + color: selected || highlightMarker + ? theme.colorScheme.secondaryContainer.withAlpha( + 128, + ) + : Colors.transparent, + ), + ), + ), + Row( + crossAxisAlignment: .start, + mainAxisAlignment: rowMainAxisAlignment, + children: [ + if (longPressSelect && !event.redacted) + SizedBox( + height: 32, + width: Avatar.defaultSize, + child: IconButton( + padding: EdgeInsets.zero, + tooltip: L10n.of(context).select, + icon: Icon( + selected + ? Icons.check_circle + : Icons.circle_outlined, + ), + onPressed: () => onSelect(event), + ), + ) + else if (nextEventSameSender || ownMessage) + SizedBox( + width: Avatar.defaultSize, + child: Center( + child: SizedBox( + width: 16, + height: 16, + child: event.status == EventStatus.error + ? const Icon(Icons.error, color: Colors.red) + : event.fileSendingStatus != null + ? const CircularProgressIndicator.adaptive( + strokeWidth: 1, + ) + : null, + ), + ), + ) + else + FutureBuilder( + future: event.fetchSenderUser(), + builder: (context, snapshot) { + final user = snapshot.data ?? sender; + return Avatar( + mxContent: user.avatarUrl, + name: user.calcDisplayname(), + onTap: () => showMemberActionsPopupMenu( + context: context, + user: user, + onMention: onMention, + ), + presenceUserId: user.stateKey, + presenceBackgroundColor: wallpaperMode + ? Colors.transparent + : null, + ); + }, + ), + Expanded( + child: Column( + crossAxisAlignment: .start, + mainAxisSize: .min, children: [ - Positioned( - top: 0, - bottom: 0, - left: 0, - right: 0, - child: InkWell( - hoverColor: longPressSelect - ? Colors.transparent - : null, - enableFeedback: !selected, - onTap: longPressSelect - ? null - : () => onSelect(event), - borderRadius: BorderRadius.circular( - AppConfig.borderRadius / 2, + if (!nextEventSameSender) + Padding( + padding: const EdgeInsets.only( + left: 8.0, + bottom: 4, ), - child: Material( - borderRadius: BorderRadius.circular( - AppConfig.borderRadius / 2, + child: ownMessage || event.room.isDirectChat + ? const SizedBox(height: 12) + : Row( + children: [ + if (sender.powerLevel >= 50) + Padding( + padding: const EdgeInsets.only( + right: 2.0, + ), + child: Icon( + sender.powerLevel >= 100 + ? Icons + .admin_panel_settings + : Icons + .add_moderator_outlined, + size: 14, + color: theme + .colorScheme + .onPrimaryContainer, + ), + ), + Expanded( + child: FutureBuilder( + future: event.fetchSenderUser(), + builder: (context, snapshot) { + final displayname = + snapshot.data + ?.calcDisplayname() ?? + sender.calcDisplayname(); + return Text( + displayname, + style: TextStyle( + fontSize: 11, + 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, + ), + ], + ), + maxLines: 1, + overflow: + TextOverflow.ellipsis, + ); + }, + ), + ), + ], + ), + ), + Container( + alignment: alignment, + padding: const EdgeInsets.only(left: 8), + child: GestureDetector( + onLongPress: longPressSelect + ? null + : () { + HapticFeedback.heavyImpact(); + onSelect(event); + }, + child: Container( + decoration: BoxDecoration( + color: noBubble + ? Colors.transparent + : color, + borderRadius: borderRadius, + ), + clipBehavior: Clip.antiAlias, + child: BubbleBackground( + colors: colors, + ignore: + noBubble || + !ownMessage || + MediaQuery.highContrastOf(context), + scrollController: scrollController, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + ), + constraints: const BoxConstraints( + maxWidth: + FluffyThemes.columnWidth * 1.5, + ), + child: Column( + mainAxisSize: .min, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + if (event.inReplyToEventId( + includingFallback: false, + ) != + null) + FutureBuilder( + 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, + ), + 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, + ), + ), + ), + ), + ); + }, + ), + MessageContent( + displayEvent, + textColor: textColor, + linkColor: linkColor, + onInfoTab: onInfoTab, + borderRadius: borderRadius, + timeline: timeline, + selected: selected, + bigEmojis: bigEmojis, + ), + if (event.hasAggregatedEvents( + timeline, + RelationshipTypes.edit, + )) + Padding( + padding: const EdgeInsets.only( + bottom: 8.0, + left: 16.0, + right: 16.0, + ), + child: Row( + mainAxisSize: + MainAxisSize.min, + spacing: 4.0, + children: [ + Icon( + Icons.edit_outlined, + color: textColor + .withAlpha(164), + size: 14, + ), + Text( + displayEvent + .originServerTs + .localizedTimeShort( + context, + ), + style: TextStyle( + color: textColor + .withAlpha(164), + fontSize: 11, + ), + ), + ], + ), + ), + ], + ), + ), ), - color: selected || highlightMarker - ? theme.colorScheme.secondaryContainer - .withAlpha(128) - : Colors.transparent, ), ), ), - Row( - crossAxisAlignment: .start, - mainAxisAlignment: rowMainAxisAlignment, - children: [ - if (longPressSelect && !event.redacted) - SizedBox( - height: 32, - width: Avatar.defaultSize, - child: IconButton( - padding: EdgeInsets.zero, - tooltip: L10n.of(context).select, - icon: Icon( - selected - ? Icons.check_circle - : Icons.circle_outlined, - ), - onPressed: () => onSelect(event), - ), - ) - else if (nextEventSameSender || ownMessage) - SizedBox( - width: Avatar.defaultSize, - child: Center( - child: SizedBox( - width: 16, - height: 16, - child: - event.status == EventStatus.error - ? const Icon( - Icons.error, - color: Colors.red, - ) - : event.fileSendingStatus != null - ? const CircularProgressIndicator.adaptive( - strokeWidth: 1, - ) - : null, - ), - ), - ) - else - FutureBuilder( - future: event.fetchSenderUser(), - builder: (context, snapshot) { - final user = snapshot.data ?? sender; - return Avatar( - mxContent: user.avatarUrl, - name: user.calcDisplayname(), - onTap: () => - showMemberActionsPopupMenu( - context: context, - user: user, - onMention: onMention, - ), - presenceUserId: user.stateKey, - presenceBackgroundColor: wallpaperMode - ? Colors.transparent - : null, - ); - }, - ), - Expanded( - child: Column( - crossAxisAlignment: .start, - mainAxisSize: .min, - children: [ - if (!nextEventSameSender) - Padding( - padding: const EdgeInsets.only( - left: 8.0, - bottom: 4, + Align( + alignment: ownMessage + ? Alignment.bottomRight + : Alignment.bottomLeft, + child: AnimatedSize( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + child: showReactionPicker + ? Padding( + padding: const EdgeInsets.all(4.0), + child: Material( + elevation: 4, + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, ), - child: - ownMessage || - event.room.isDirectChat - ? const SizedBox(height: 12) - : Row( - children: [ - if (sender.powerLevel >= - 50) - Padding( - padding: - const EdgeInsets.only( - right: 2.0, - ), - child: Icon( - sender.powerLevel >= - 100 - ? Icons - .admin_panel_settings - : Icons - .add_moderator_outlined, - size: 14, - color: theme - .colorScheme - .onPrimaryContainer, - ), - ), - Expanded( - child: FutureBuilder( - future: event - .fetchSenderUser(), - builder: (context, snapshot) { - final displayname = - snapshot.data - ?.calcDisplayname() ?? - sender - .calcDisplayname(); - return Text( - displayname, - style: TextStyle( - fontSize: 11, - 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, - ), - ], - ), - maxLines: 1, - overflow: - TextOverflow - .ellipsis, - ); - }, - ), - ), - ], - ), - ), - Container( - alignment: alignment, - padding: const EdgeInsets.only( - left: 8, - ), - child: GestureDetector( - onLongPress: longPressSelect - ? null - : () { - HapticFeedback.heavyImpact(); - onSelect(event); - }, - child: AnimatedOpacity( - opacity: animateIn - ? 0 - : event.messageType == - MessageTypes - .BadEncrypted || - event.status.isSending - ? 0.5 - : 1, - duration: FluffyThemes - .animationDuration, - curve: - FluffyThemes.animationCurve, - child: Container( - decoration: BoxDecoration( - color: noBubble - ? Colors.transparent - : color, - borderRadius: borderRadius, - ), - clipBehavior: Clip.antiAlias, - child: BubbleBackground( - colors: colors, - ignore: - noBubble || - !ownMessage || - MediaQuery.highContrastOf( - context, - ), - scrollController: - scrollController, - child: Container( - decoration: BoxDecoration( - borderRadius: - BorderRadius.circular( - AppConfig - .borderRadius, - ), - ), - constraints: - const BoxConstraints( - maxWidth: - FluffyThemes - .columnWidth * - 1.5, - ), - child: Column( - mainAxisSize: .min, - crossAxisAlignment: - CrossAxisAlignment - .start, - children: [ - if (event.inReplyToEventId( - includingFallback: - false, - ) != - null) - FutureBuilder( - future: event - .getReplyEvent( - timeline, + shadowColor: theme + .colorScheme + .surface + .withAlpha(128), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisSize: .min, + children: [ + ...AppConfig.defaultReactions.map( + (emoji) => IconButton( + padding: EdgeInsets.zero, + icon: Center( + child: Opacity( + opacity: + sentReactions + .contains( + emoji, + ) + ? 0.33 + : 1, + child: Text( + emoji, + style: + const TextStyle( + fontSize: 20, ), - 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, - ), - ), - ), - ), + textAlign: TextAlign + .center, + ), + ), + ), + onPressed: + sentReactions + .contains(emoji) + ? null + : () { + onSelect(event); + event.room + .sendReaction( + event + .eventId, + emoji, ); - }, - ), - MessageContent( - displayEvent, - textColor: textColor, - linkColor: linkColor, - onInfoTab: onInfoTab, - borderRadius: - borderRadius, - timeline: timeline, - selected: selected, - bigEmojis: bigEmojis, - ), - if (event - .hasAggregatedEvents( - timeline, - RelationshipTypes - .edit, - )) - Padding( - padding: - const EdgeInsets.only( - bottom: 8.0, - left: 16.0, - right: 16.0, - ), - child: Row( - mainAxisSize: - MainAxisSize - .min, - spacing: 4.0, - children: [ - Icon( - Icons - .edit_outlined, - color: textColor - .withAlpha( - 164, - ), - size: 14, - ), - Text( - displayEvent - .originServerTs - .localizedTimeShort( - context, - ), - style: TextStyle( - color: textColor - .withAlpha( - 164, - ), - fontSize: - 11, - ), - ), - ], - ), - ), - ], + }, ), ), - ), - ), - ), - ), - ), - Align( - alignment: ownMessage - ? Alignment.bottomRight - : Alignment.bottomLeft, - child: AnimatedSize( - duration: - FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - child: showReactionPicker - ? Padding( - padding: - const EdgeInsets.all( - 4.0, - ), - child: Material( - elevation: 4, - borderRadius: - BorderRadius.circular( - AppConfig - .borderRadius, - ), - shadowColor: theme - .colorScheme - .surface - .withAlpha(128), - child: SingleChildScrollView( - scrollDirection: - Axis.horizontal, - child: Row( - mainAxisSize: .min, - children: [ - ...AppConfig.defaultReactions.map( - ( - emoji, - ) => IconButton( - padding: - EdgeInsets - .zero, - icon: Center( - child: Opacity( - opacity: - sentReactions.contains( - emoji, - ) - ? 0.33 - : 1, - child: Text( - emoji, - style: const TextStyle( - fontSize: - 20, - ), - textAlign: - TextAlign - .center, - ), - ), - ), - onPressed: - sentReactions - .contains( - emoji, - ) - ? null - : () { - onSelect( - event, - ); - event.room.sendReaction( - event - .eventId, - emoji, - ); - }, - ), - ), - IconButton( - icon: const Icon( - Icons - .add_reaction_outlined, - ), - tooltip: L10n.of( + IconButton( + icon: const Icon( + Icons + .add_reaction_outlined, + ), + tooltip: L10n.of( + context, + ).customReaction, + onPressed: () async { + final emoji = await showAdaptiveBottomSheet( + context: context, + builder: (context) => Scaffold( + appBar: AppBar( + title: Text( + L10n.of( context, ).customReaction, - onPressed: () async { - final emoji = await showAdaptiveBottomSheet( - context: - context, - builder: (context) => Scaffold( - appBar: AppBar( - title: Text( - L10n.of( - context, - ).customReaction, - ), - leading: CloseButton( - onPressed: () => Navigator.of( - context, - ).pop(null), - ), - ), - body: SizedBox( - height: double - .infinity, - child: EmojiPicker( - onEmojiSelected: - ( - _, - emoji, - ) => - Navigator.of( - context, - ).pop( - emoji.emoji, - ), - config: Config( - locale: Localizations.localeOf( - context, - ), - emojiViewConfig: const EmojiViewConfig( - backgroundColor: - Colors.transparent, - ), - bottomActionBarConfig: const BottomActionBarConfig( - enabled: - false, - ), - categoryViewConfig: CategoryViewConfig( - 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, - ), - skinToneConfig: SkinToneConfig( - dialogBackgroundColor: Color.lerp( - theme.colorScheme.surface, - theme.colorScheme.primaryContainer, - 0.75, - )!, - indicatorColor: - theme.colorScheme.onSurface, - ), - ), - ), - ), - ), - ); - if (emoji == - null) { - return; - } - if (sentReactions - .contains( - emoji, - )) { - return; - } - onSelect(event); - - await event.room - .sendReaction( - event - .eventId, - emoji, - ); - }, ), - ], + leading: CloseButton( + onPressed: () => + Navigator.of( + context, + ).pop(null), + ), + ), + body: SizedBox( + height: + double.infinity, + child: EmojiPicker( + onEmojiSelected: + (_, emoji) => + Navigator.of( + context, + ).pop( + emoji + .emoji, + ), + config: Config( + locale: + Localizations.localeOf( + context, + ), + emojiViewConfig: + const EmojiViewConfig( + backgroundColor: + Colors + .transparent, + ), + bottomActionBarConfig: + const BottomActionBarConfig( + enabled: + false, + ), + categoryViewConfig: CategoryViewConfig( + 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, + ), + skinToneConfig: SkinToneConfig( + dialogBackgroundColor: Color.lerp( + theme + .colorScheme + .surface, + theme + .colorScheme + .primaryContainer, + 0.75, + )!, + indicatorColor: theme + .colorScheme + .onSurface, + ), + ), + ), + ), ), - ), - ), - ) - : const SizedBox.shrink(), + ); + if (emoji == null) { + return; + } + if (sentReactions + .contains(emoji)) { + return; + } + onSelect(event); + + await event.room + .sendReaction( + event.eventId, + emoji, + ); + }, + ), + ], + ), + ), ), - ), - ], - ), - ), - ], + ) + : const SizedBox.shrink(), + ), ), ], ), - ); - }, - ), - - AnimatedSize( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - alignment: Alignment.bottomCenter, - child: !hasReactions - ? const SizedBox.shrink() - : Padding( - padding: EdgeInsets.only( - top: 1.0, - left: (ownMessage ? 0 : Avatar.defaultSize) + 12.0, - right: ownMessage ? 0 : 12.0, ), - child: MessageReactions(event, timeline), - ), - ), - if (enterThread != null) + ], + ), + ], + ), + AnimatedSize( duration: FluffyThemes.animationDuration, curve: FluffyThemes.animationCurve, alignment: Alignment.bottomCenter, - child: threadChildren.isEmpty + child: !hasReactions ? const SizedBox.shrink() : Padding( - padding: const EdgeInsets.only( - top: 2.0, - bottom: 8.0, - left: Avatar.defaultSize + 8, + padding: EdgeInsets.only( + top: 1.0, + left: (ownMessage ? 0 : Avatar.defaultSize) + 12.0, + right: ownMessage ? 0 : 12.0, ), - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: FluffyThemes.columnWidth * 1.5, + child: MessageReactions(event, timeline), + ), + ), + if (enterThread != null) + AnimatedSize( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + alignment: Alignment.bottomCenter, + child: threadChildren.isEmpty + ? const SizedBox.shrink() + : Padding( + padding: const EdgeInsets.only( + top: 2.0, + bottom: 8.0, + left: Avatar.defaultSize + 8, ), - child: TextButton.icon( - style: TextButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), + child: ConstrainedBox( + 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, + ), + onPressed: () => enterThread(event.eventId), + icon: const Icon(Icons.message), + label: Text( + '${L10n.of(context).countReplies(threadChildren.length)} | ${threadChildren.first.calcLocalizedBodyFallback(MatrixLocals(L10n.of(context)), withSenderNamePrefix: true)}', + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - foregroundColor: - theme.colorScheme.onSecondaryContainer, - backgroundColor: - theme.colorScheme.secondaryContainer, - ), - onPressed: () => enterThread(event.eventId), - icon: const Icon(Icons.message), - label: Text( - '${L10n.of(context).countReplies(threadChildren.length)} | ${threadChildren.first.calcLocalizedBodyFallback(MatrixLocals(L10n.of(context)), withSenderNamePrefix: true)}', - maxLines: 1, - overflow: TextOverflow.ellipsis, ), ), ), - ), - ), - if (displayReadMarker) - Row( - children: [ - 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, - ), - decoration: BoxDecoration( - 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, + ), + 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, + ), + decoration: BoxDecoration( + 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, + ), + ), + ], + ), + ], + ), ), ), ), @@ -1033,3 +936,37 @@ class BubblePainter extends CustomPainter { return scrollable.position != oldScrollable?.position; } } + +class _AnimateIn extends StatefulWidget { + final bool animateIn; + final Widget child; + const _AnimateIn({required this.animateIn, required this.child}); + + @override + State<_AnimateIn> createState() => __AnimateInState(); +} + +class __AnimateInState extends State<_AnimateIn> { + bool _animationFinished = false; + @override + Widget build(BuildContext context) { + if (!widget.animateIn) return widget.child; + if (!_animationFinished) { + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + _animationFinished = true; + }); + }); + } + return AnimatedOpacity( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + opacity: _animationFinished ? 1 : 0, + child: AnimatedSize( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + child: _animationFinished ? widget.child : const SizedBox.shrink(), + ), + ); + } +} diff --git a/lib/pages/settings_style/settings_style.dart b/lib/pages/settings_style/settings_style.dart index c47495686..273d2684f 100644 --- a/lib/pages/settings_style/settings_style.dart +++ b/lib/pages/settings_style/settings_style.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:file_picker/file_picker.dart'; -import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/utils/account_config.dart'; import 'package:fluffychat/utils/file_selector.dart'; @@ -111,32 +110,6 @@ class SettingsStyleController extends State { ThemeMode get currentTheme => ThemeController.of(context).themeMode; Color? get currentColor => ThemeController.of(context).primaryColor; - static final List customColors = [ - null, - AppConfig.chatColor, - Colors.indigo, - Colors.blue, - Colors.blueAccent, - Colors.teal, - Colors.tealAccent, - Colors.green, - Colors.greenAccent, - Colors.yellow, - Colors.yellowAccent, - Colors.orange, - Colors.orangeAccent, - Colors.red, - Colors.redAccent, - Colors.pink, - Colors.pinkAccent, - Colors.purple, - Colors.purpleAccent, - Colors.blueGrey, - Colors.grey, - Colors.white, - Colors.black, - ]; - void switchTheme(ThemeMode? newTheme) { if (newTheme == null) return; switch (newTheme) { diff --git a/lib/pages/settings_style/settings_style_view.dart b/lib/pages/settings_style/settings_style_view.dart index 9391bbf3f..e29450771 100644 --- a/lib/pages/settings_style/settings_style_view.dart +++ b/lib/pages/settings_style/settings_style_view.dart @@ -82,14 +82,13 @@ class SettingsStyleView extends StatelessWidget { Theme.of(context).brightness == Brightness.light ? light?.primary : dark?.primary; - final colors = List.from( - SettingsStyleController.customColors, - ); + final colors = [null, AppConfig.chatColor, ...Colors.primaries]; if (systemColor == null) { colors.remove(null); } return GridView.builder( shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 64, ),