diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index c234a102f..2324b73d0 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -1577,6 +1577,7 @@ "username": {} } }, + "directChat": "Direct chat", "redactedByBecause": "Redacted by {username} because: \"{reason}\"", "@redactedByBecause": { "type": "text", diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 9ab086fc9..c8ef93a58 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -20,9 +20,11 @@ import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat/chat_view.dart'; import 'package:fluffychat/pages/chat/event_info_dialog.dart'; import 'package:fluffychat/pages/chat/recording_dialog.dart'; +import 'package:fluffychat/pages/chat_details/chat_details.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/error_reporter.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; @@ -61,10 +63,31 @@ class ChatPage extends StatelessWidget { ), ); } - return ChatPageWithRoom( - key: Key('chat_page_$roomId'), - sideView: sideView, - room: room, + + return Row( + children: [ + Expanded( + child: ChatPageWithRoom( + key: Key('chat_page_$roomId'), + sideView: sideView, + room: room, + ), + ), + if (FluffyThemes.isThreeColumnMode(context)) + Container( + width: FluffyThemes.columnWidth, + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + border: Border( + left: BorderSide( + width: 1, + color: Theme.of(context).dividerColor, + ), + ), + ), + child: ChatDetails(roomId: roomId), + ), + ], ); } } diff --git a/lib/pages/chat/chat_app_bar_title.dart b/lib/pages/chat/chat_app_bar_title.dart index 2cbbfb216..6f022b3f1 100644 --- a/lib/pages/chat/chat_app_bar_title.dart +++ b/lib/pages/chat/chat_app_bar_title.dart @@ -4,8 +4,6 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:fluffychat/pages/chat/chat.dart'; -import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; -import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/avatar.dart'; @@ -19,25 +17,13 @@ class ChatAppBarTitle extends StatelessWidget { if (controller.selectedEvents.isNotEmpty) { return Text(controller.selectedEvents.length.toString()); } - final directChatMatrixID = room.directChatMatrixID; return InkWell( hoverColor: Colors.transparent, splashColor: Colors.transparent, highlightColor: Colors.transparent, - onTap: directChatMatrixID != null - ? () => showAdaptiveBottomSheet( - context: context, - builder: (c) => UserBottomSheet( - user: room - .unsafeGetUserFromMemoryOrFallback(directChatMatrixID), - outerContext: context, - onMention: () => controller.sendController.text += - '${room.unsafeGetUserFromMemoryOrFallback(directChatMatrixID).mention} ', - ), - ) - : controller.isArchived - ? null - : () => context.go(['', 'rooms', room.id, 'details'].join('/')), + onTap: controller.isArchived + ? null + : () => context.go(['', 'rooms', room.id, 'details'].join('/')), child: Row( children: [ Hero( diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index cf3e1cd60..346451c62 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -131,7 +131,7 @@ class ChatView extends StatelessWidget { tooltip: L10n.of(context)!.placeCall, ), EncryptionButton(controller.room), - ChatSettingsPopupMenu(controller.room, !controller.room.isDirectChat), + ChatSettingsPopupMenu(controller.room, true), ]; } } diff --git a/lib/pages/chat_details/chat_details.dart b/lib/pages/chat_details/chat_details.dart index 3b3f424b8..795ab9c4e 100644 --- a/lib/pages/chat_details/chat_details.dart +++ b/lib/pages/chat_details/chat_details.dart @@ -318,9 +318,9 @@ class ChatDetailsController extends State { if ((room.states['im.ponies.room_emotes'] ?? {}) .keys .any((String s) => s.isNotEmpty)) { - context.go('/rooms/${room.id}/details/multiple_emotes'); + context.push('/rooms/${room.id}/details/multiple_emotes'); } else { - context.go('/rooms/${room.id}/details/emotes'); + context.push('/rooms/${room.id}/details/emotes'); } } diff --git a/lib/pages/chat_details/chat_details_view.dart b/lib/pages/chat_details/chat_details_view.dart index 5809a6804..8a8c63cf1 100644 --- a/lib/pages/chat_details/chat_details_view.dart +++ b/lib/pages/chat_details/chat_details_view.dart @@ -35,6 +35,8 @@ class ChatDetailsView extends StatelessWidget { ); } + final isEmbedded = GoRouterState.of(context).fullPath == '/rooms/:roomid'; + return StreamBuilder( stream: room.onUpdate.stream, builder: (context, snapshot) { @@ -49,24 +51,27 @@ class ChatDetailsView extends StatelessWidget { MatrixLocals(L10n.of(context)!), ); return Scaffold( - appBar: AppBar( - leading: const Center(child: BackButton()), - elevation: Theme.of(context).appBarTheme.elevation, - actions: [ - if (room.canonicalAlias.isNotEmpty) - IconButton( - tooltip: L10n.of(context)!.share, - icon: Icon(Icons.adaptive.share_outlined), - onPressed: () => FluffyShare.share( - AppConfig.inviteLinkPrefix + room.canonicalAlias, - context, - ), + appBar: isEmbedded + ? null + : AppBar( + leading: const Center(child: BackButton()), + elevation: Theme.of(context).appBarTheme.elevation, + actions: [ + if (room.canonicalAlias.isNotEmpty) + IconButton( + tooltip: L10n.of(context)!.share, + icon: Icon(Icons.adaptive.share_outlined), + onPressed: () => FluffyShare.share( + AppConfig.inviteLinkPrefix + room.canonicalAlias, + context, + ), + ), + ChatSettingsPopupMenu(room, false) + ], + title: Text(L10n.of(context)!.chatDetails), + backgroundColor: + Theme.of(context).appBarTheme.backgroundColor, ), - ChatSettingsPopupMenu(room, false) - ], - title: Text(L10n.of(context)!.chatDetails), - backgroundColor: Theme.of(context).appBarTheme.backgroundColor, - ), body: MaxWidthBody( child: ListView.builder( physics: const NeverScrollableScrollPhysics(), @@ -99,7 +104,9 @@ class ChatDetailsView extends StatelessWidget { ), ), child: Hero( - tag: 'content_banner', + tag: isEmbedded + ? 'embedded_content_banner' + : 'content_banner', child: Avatar( mxContent: room.avatar, name: displayname, @@ -108,9 +115,10 @@ class ChatDetailsView extends StatelessWidget { ), ), ), - if (room.canChangeStateEvent( - EventTypes.RoomAvatar, - )) + if (!room.isDirectChat && + room.canChangeStateEvent( + EventTypes.RoomAvatar, + )) Positioned( bottom: 0, right: 0, @@ -131,21 +139,25 @@ class ChatDetailsView extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ TextButton.icon( - onPressed: () => room.canChangeStateEvent( - EventTypes.RoomName, - ) - ? controller.setDisplaynameAction() - : FluffyShare.share( - displayname, - context, - copyOnly: true, - ), + onPressed: () => room.isDirectChat + ? null + : room.canChangeStateEvent( + EventTypes.RoomName, + ) + ? controller.setDisplaynameAction() + : FluffyShare.share( + displayname, + context, + copyOnly: true, + ), icon: Icon( - room.canChangeStateEvent( - EventTypes.RoomName, - ) - ? Icons.edit_outlined - : Icons.copy_outlined, + room.isDirectChat + ? Icons.chat_bubble_outline + : room.canChangeStateEvent( + EventTypes.RoomName, + ) + ? Icons.edit_outlined + : Icons.copy_outlined, size: 16, ), style: TextButton.styleFrom( @@ -154,16 +166,20 @@ class ChatDetailsView extends StatelessWidget { .onBackground, ), label: Text( - displayname, + room.isDirectChat + ? L10n.of(context)!.directChat + : displayname, maxLines: 1, overflow: TextOverflow.ellipsis, // style: const TextStyle(fontSize: 18), ), ), TextButton.icon( - onPressed: () => context.go( - '/rooms/${controller.roomId}/details/members', - ), + onPressed: () => room.isDirectChat + ? null + : context.push( + '/rooms/${controller.roomId}/details/members', + ), icon: const Icon( Icons.group_outlined, size: 14, @@ -272,44 +288,46 @@ class ChatDetailsView extends StatelessWidget { onTap: controller.goToEmoteSettings, trailing: const Icon(Icons.chevron_right_outlined), ), - ListTile( - leading: CircleAvatar( - backgroundColor: - Theme.of(context).scaffoldBackgroundColor, - foregroundColor: iconColor, - child: const Icon(Icons.shield_outlined), - ), - title: Text( - L10n.of(context)!.whoIsAllowedToJoinThisGroup, - ), - trailing: const Icon(Icons.chevron_right_outlined), - subtitle: Text( - room.joinRules?.getLocalizedString( - MatrixLocals(L10n.of(context)!), - ) ?? - L10n.of(context)!.none, - ), - onTap: controller.setJoinRules, - ), - ListTile( - leading: CircleAvatar( - backgroundColor: - Theme.of(context).scaffoldBackgroundColor, - foregroundColor: iconColor, - child: const Icon(Icons.visibility_outlined), - ), - trailing: const Icon(Icons.chevron_right_outlined), - title: Text( - L10n.of(context)!.visibilityOfTheChatHistory, + if (!room.isDirectChat) + ListTile( + leading: CircleAvatar( + backgroundColor: + Theme.of(context).scaffoldBackgroundColor, + foregroundColor: iconColor, + child: const Icon(Icons.shield_outlined), + ), + title: Text( + L10n.of(context)!.whoIsAllowedToJoinThisGroup, + ), + trailing: const Icon(Icons.chevron_right_outlined), + subtitle: Text( + room.joinRules?.getLocalizedString( + MatrixLocals(L10n.of(context)!), + ) ?? + L10n.of(context)!.none, + ), + onTap: controller.setJoinRules, ), - subtitle: Text( - room.historyVisibility?.getLocalizedString( - MatrixLocals(L10n.of(context)!), - ) ?? - L10n.of(context)!.none, + if (!room.isDirectChat) + ListTile( + leading: CircleAvatar( + backgroundColor: + Theme.of(context).scaffoldBackgroundColor, + foregroundColor: iconColor, + child: const Icon(Icons.visibility_outlined), + ), + trailing: const Icon(Icons.chevron_right_outlined), + title: Text( + L10n.of(context)!.visibilityOfTheChatHistory, + ), + subtitle: Text( + room.historyVisibility?.getLocalizedString( + MatrixLocals(L10n.of(context)!), + ) ?? + L10n.of(context)!.none, + ), + onTap: controller.setHistoryVisibility, ), - onTap: controller.setHistoryVisibility, - ), if (room.joinRules == JoinRules.public) ListTile( leading: CircleAvatar( @@ -331,23 +349,24 @@ class ChatDetailsView extends StatelessWidget { ), onTap: controller.setGuestAccess, ), - ListTile( - title: Text(L10n.of(context)!.editChatPermissions), - subtitle: Text( - L10n.of(context)!.whoCanPerformWhichAction, - ), - leading: CircleAvatar( - backgroundColor: - Theme.of(context).scaffoldBackgroundColor, - foregroundColor: iconColor, - child: const Icon( - Icons.edit_attributes_outlined, + if (!room.isDirectChat) + ListTile( + title: Text(L10n.of(context)!.editChatPermissions), + subtitle: Text( + L10n.of(context)!.whoCanPerformWhichAction, ), + leading: CircleAvatar( + backgroundColor: + Theme.of(context).scaffoldBackgroundColor, + foregroundColor: iconColor, + child: const Icon( + Icons.edit_attributes_outlined, + ), + ), + trailing: const Icon(Icons.chevron_right_outlined), + onTap: () => context + .push('/rooms/${room.id}/details/permissions'), ), - trailing: const Icon(Icons.chevron_right_outlined), - onTap: () => context - .go('/rooms/${room.id}/details/permissions'), - ), Divider( height: 1, color: Theme.of(context).dividerColor, @@ -363,7 +382,7 @@ class ChatDetailsView extends StatelessWidget { ), ), ), - if (room.canInvite) + if (!room.isDirectChat && room.canInvite) ListTile( title: Text(L10n.of(context)!.inviteContact), leading: CircleAvatar( @@ -393,7 +412,7 @@ class ChatDetailsView extends StatelessWidget { color: Colors.grey, ), ), - onTap: () => context.go( + onTap: () => context.push( '/rooms/${controller.roomId!}/details/members', ), trailing: const Icon(Icons.chevron_right_outlined), diff --git a/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart b/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart index d3f1b1a7c..8b0cdc03f 100644 --- a/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart +++ b/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart @@ -9,6 +9,7 @@ import 'package:url_launcher/url_launcher_string.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat_encryption_settings/chat_encryption_settings.dart'; import 'package:fluffychat/utils/beautify_string_extension.dart'; +import 'package:fluffychat/widgets/layouts/max_width_body.dart'; class ChatEncryptionSettingsView extends StatelessWidget { final ChatEncryptionSettingsController controller; @@ -38,159 +39,165 @@ class ChatEncryptionSettingsView extends StatelessWidget { ), ], ), - body: ListView( - children: [ - SwitchListTile( - secondary: CircleAvatar( - foregroundColor: - Theme.of(context).colorScheme.onPrimaryContainer, - backgroundColor: Theme.of(context).colorScheme.primaryContainer, - child: const Icon(Icons.lock_outlined), + body: MaxWidthBody( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SwitchListTile( + secondary: CircleAvatar( + foregroundColor: + Theme.of(context).colorScheme.onPrimaryContainer, + backgroundColor: + Theme.of(context).colorScheme.primaryContainer, + child: const Icon(Icons.lock_outlined), + ), + title: Text(L10n.of(context)!.encryptThisChat), + value: room.encrypted, + onChanged: controller.enableEncryption, ), - title: Text(L10n.of(context)!.encryptThisChat), - value: room.encrypted, - onChanged: controller.enableEncryption, - ), - Icon( - CupertinoIcons.lock_shield, - size: 128, - color: Theme.of(context).colorScheme.onInverseSurface, - ), - const Divider(), - if (room.isDirectChat) - Padding( - padding: const EdgeInsets.all(16.0), - child: SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: controller.startVerification, - icon: const Icon(Icons.verified_outlined), - label: Text(L10n.of(context)!.verifyStart), + Icon( + CupertinoIcons.lock_shield, + size: 128, + color: Theme.of(context).colorScheme.onInverseSurface, + ), + const Divider(), + if (room.isDirectChat) + Padding( + padding: const EdgeInsets.all(16.0), + child: SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: controller.startVerification, + icon: const Icon(Icons.verified_outlined), + label: Text(L10n.of(context)!.verifyStart), + ), ), ), - ), - if (room.encrypted) ...[ - const SizedBox(height: 16), - ListTile( - title: Text( - L10n.of(context)!.deviceKeys, - style: const TextStyle( - fontWeight: FontWeight.bold, + if (room.encrypted) ...[ + const SizedBox(height: 16), + ListTile( + title: Text( + L10n.of(context)!.deviceKeys, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), ), ), - ), - StreamBuilder( - stream: room.onUpdate.stream, - builder: (context, snapshot) => FutureBuilder>( - future: room.getUserDeviceKeys(), - builder: (BuildContext context, snapshot) { - if (snapshot.hasError) { - return Center( - child: Text( - '${L10n.of(context)!.oopsSomethingWentWrong}: ${snapshot.error}', - ), - ); - } - if (!snapshot.hasData) { - return const Center( - child: CircularProgressIndicator.adaptive( - strokeWidth: 2, - ), - ); - } - final deviceKeys = snapshot.data!; - return ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: deviceKeys.length, - itemBuilder: (BuildContext context, int i) => - SwitchListTile( - value: !deviceKeys[i].blocked, - activeColor: deviceKeys[i].verified - ? Colors.green - : Colors.orange, - onChanged: (_) => - controller.toggleDeviceKey(deviceKeys[i]), - title: Row( - children: [ - Icon( - deviceKeys[i].verified - ? Icons.verified_outlined - : deviceKeys[i].blocked - ? Icons.block_outlined - : Icons.info_outlined, - color: deviceKeys[i].verified - ? Colors.green - : deviceKeys[i].blocked - ? Colors.red - : Colors.orange, - size: 20, - ), - const SizedBox(width: 4), - Text( - deviceKeys[i].deviceId ?? - L10n.of(context)!.unknownDevice, - ), - const SizedBox(width: 4), - Flexible( - fit: FlexFit.loose, - child: Material( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - AppConfig.borderRadius, - ), - side: BorderSide( - color: - Theme.of(context).colorScheme.primary, - ), - ), - color: Theme.of(context) - .colorScheme - .primaryContainer, - child: Padding( - padding: const EdgeInsets.all(4.0), - child: Text( - deviceKeys[i].userId, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( + StreamBuilder( + stream: room.onUpdate.stream, + builder: (context, snapshot) => + FutureBuilder>( + future: room.getUserDeviceKeys(), + builder: (BuildContext context, snapshot) { + if (snapshot.hasError) { + return Center( + child: Text( + '${L10n.of(context)!.oopsSomethingWentWrong}: ${snapshot.error}', + ), + ); + } + if (!snapshot.hasData) { + return const Center( + child: CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), + ); + } + final deviceKeys = snapshot.data!; + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: deviceKeys.length, + itemBuilder: (BuildContext context, int i) => + SwitchListTile( + value: !deviceKeys[i].blocked, + activeColor: deviceKeys[i].verified + ? Colors.green + : Colors.orange, + onChanged: (_) => + controller.toggleDeviceKey(deviceKeys[i]), + title: Row( + children: [ + Icon( + deviceKeys[i].verified + ? Icons.verified_outlined + : deviceKeys[i].blocked + ? Icons.block_outlined + : Icons.info_outlined, + color: deviceKeys[i].verified + ? Colors.green + : deviceKeys[i].blocked + ? Colors.red + : Colors.orange, + size: 20, + ), + const SizedBox(width: 4), + Text( + deviceKeys[i].deviceId ?? + L10n.of(context)!.unknownDevice, + ), + const SizedBox(width: 4), + Flexible( + fit: FlexFit.loose, + child: Material( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + side: BorderSide( color: Theme.of(context).colorScheme.primary, - fontSize: 12, - fontStyle: FontStyle.italic, + ), + ), + color: Theme.of(context) + .colorScheme + .primaryContainer, + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Text( + deviceKeys[i].userId, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Theme.of(context) + .colorScheme + .primary, + fontSize: 12, + fontStyle: FontStyle.italic, + ), ), ), ), ), + ], + ), + subtitle: Text( + deviceKeys[i].ed25519Key?.beautified ?? + L10n.of(context)!.unknownEncryptionAlgorithm, + style: TextStyle( + fontFamily: 'RobotoMono', + color: Theme.of(context).colorScheme.secondary, ), - ], - ), - subtitle: Text( - deviceKeys[i].ed25519Key?.beautified ?? - L10n.of(context)!.unknownEncryptionAlgorithm, - style: TextStyle( - fontFamily: 'RobotoMono', - color: Theme.of(context).colorScheme.secondary, ), ), - ), - ); - }, + ); + }, + ), ), - ), - ] else - Padding( - padding: const EdgeInsets.all(16.0), - child: Center( - child: Text( - L10n.of(context)!.encryptionNotEnabled, - style: const TextStyle( - fontStyle: FontStyle.italic, + ] else + Padding( + padding: const EdgeInsets.all(16.0), + child: Center( + child: Text( + L10n.of(context)!.encryptionNotEnabled, + style: const TextStyle( + fontStyle: FontStyle.italic, + ), ), ), ), - ), - ], + ], + ), ), ), );