diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index b9aaa894..146d423d 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2509,6 +2509,8 @@ "@passwordIsWrong": {}, "publicLink": "Public link", "@publicLink": {}, + "publicLinks": "Public links", + "createNewLink": "Create new link", "joinSpace": "Join space", "@joinSpace": {}, "publicSpaces": "Public spaces", diff --git a/lib/pages/chat_access_settings/chat_access_settings_controller.dart b/lib/pages/chat_access_settings/chat_access_settings_controller.dart index 068a47f3..822075f3 100644 --- a/lib/pages/chat_access_settings/chat_access_settings_controller.dart +++ b/lib/pages/chat_access_settings/chat_access_settings_controller.dart @@ -152,31 +152,76 @@ class ChatAccessSettingsController extends State { ); } - void setCanonicalAlias() async { + Future addAlias() async { + final domain = room.client.userID?.domain; + if (domain == null) { + throw Exception('userID or domain is null! This should never happen.'); + } + final input = await showTextInputDialog( context: context, title: L10n.of(context)!.editRoomAliases, - cancelLabel: L10n.of(context)!.cancel, - okLabel: L10n.of(context)!.ok, textFields: [ DialogTextField( prefixText: '#', - suffixText: room.client.userID!.domain!, - initialText: room.canonicalAlias.localpart, + suffixText: domain, + hintText: L10n.of(context)!.alias, ), ], ); - final newAliasLocalpart = input?.singleOrNull?.trim(); - if (newAliasLocalpart == null || newAliasLocalpart.isEmpty) return; + final aliasLocalpart = input?.singleOrNull?.trim(); + if (aliasLocalpart == null || aliasLocalpart.isEmpty) return; + final alias = '#$aliasLocalpart:$domain'; + + final result = await showFutureLoadingDialog( + context: context, + future: () => room.client.setRoomAlias(alias, room.id), + ); + if (result.error != null) return; + + final canonicalAliasConsent = await showOkCancelAlertDialog( + context: context, + title: L10n.of(context)!.setAsCanonicalAlias, + message: alias, + okLabel: L10n.of(context)!.yes, + cancelLabel: L10n.of(context)!.no, + ); + + final altAliases = room + .getState(EventTypes.RoomCanonicalAlias) + ?.content + .tryGetList('alt_aliases') + ?.toSet() ?? + {}; + if (room.canonicalAlias.isNotEmpty) altAliases.add(room.canonicalAlias); + altAliases.add(alias); + if (canonicalAliasConsent == OkCancelResult.ok) { + altAliases.remove(alias); + } else { + altAliases.remove(room.canonicalAlias); + } await showFutureLoadingDialog( context: context, - future: () => room.setCanonicalAlias( - '#$newAliasLocalpart:${room.client.userID!.domain!}', + future: () => room.client.setRoomStateWithKey( + room.id, + EventTypes.RoomCanonicalAlias, + '', + { + 'alias': canonicalAliasConsent == OkCancelResult.ok + ? alias + : room.canonicalAlias, + if (altAliases.isNotEmpty) 'alt_aliases': altAliases.toList(), + }, ), ); } + void deleteAlias(String alias) => showFutureLoadingDialog( + context: context, + future: () => room.client.deleteRoomAlias(alias), + ); + void setChatVisibilityOnDirectory(bool? visibility) async { if (visibility == null) return; setState(() { diff --git a/lib/pages/chat_access_settings/chat_access_settings_page.dart b/lib/pages/chat_access_settings/chat_access_settings_page.dart index 0d6a10bc..6e9557e0 100644 --- a/lib/pages/chat_access_settings/chat_access_settings_page.dart +++ b/lib/pages/chat_access_settings/chat_access_settings_page.dart @@ -23,157 +23,214 @@ class ChatAccessSettingsPageView extends StatelessWidget { body: MaxWidthBody( child: StreamBuilder( stream: room.onUpdate.stream, - builder: (context, snapshot) => Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - title: Text( - L10n.of(context)!.visibilityOfTheChatHistory, - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - fontWeight: FontWeight.bold, - ), - ), - ), - for (final historyVisibility in HistoryVisibility.values) - RadioListTile.adaptive( - title: Text( - historyVisibility - .getLocalizedString(MatrixLocals(L10n.of(context)!)), - ), - value: historyVisibility, - groupValue: room.historyVisibility, - onChanged: controller.historyVisibilityLoading || - !room.canChangeHistoryVisibility - ? null - : controller.setHistoryVisibility, - ), - Divider(color: Theme.of(context).dividerColor), - ListTile( - title: Text( - L10n.of(context)!.whoIsAllowedToJoinThisGroup, - style: TextStyle( - color: Theme.of(context).colorScheme.secondary, - fontWeight: FontWeight.bold, - ), - ), - ), - for (final joinRule - in JoinRules.values..remove(JoinRules.private)) - RadioListTile.adaptive( - title: Text( - joinRule.localizedString(L10n.of(context)!), - ), - value: joinRule, - groupValue: room.joinRules, - onChanged: - controller.joinRulesLoading || !room.canChangeJoinRules - ? null - : controller.setJoinRule, - ), - Divider(color: Theme.of(context).dividerColor), - if ({JoinRules.public, JoinRules.knock} - .contains(room.joinRules)) ...[ + builder: (context, snapshot) { + final canonicalAlias = room.canonicalAlias; + final altAliases = room + .getState(EventTypes.RoomCanonicalAlias) + ?.content + .tryGetList('alt_aliases') ?? + []; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ ListTile( title: Text( - L10n.of(context)!.areGuestsAllowedToJoin, + L10n.of(context)!.visibilityOfTheChatHistory, style: TextStyle( color: Theme.of(context).colorScheme.secondary, fontWeight: FontWeight.bold, ), ), ), - for (final guestAccess in GuestAccess.values) - RadioListTile.adaptive( + for (final historyVisibility in HistoryVisibility.values) + RadioListTile.adaptive( title: Text( - guestAccess + historyVisibility .getLocalizedString(MatrixLocals(L10n.of(context)!)), ), - value: guestAccess, - groupValue: room.guestAccess, - onChanged: controller.guestAccessLoading || - !room.canChangeGuestAccess + value: historyVisibility, + groupValue: room.historyVisibility, + onChanged: controller.historyVisibilityLoading || + !room.canChangeHistoryVisibility ? null - : controller.setGuestAccess, + : controller.setHistoryVisibility, ), Divider(color: Theme.of(context).dividerColor), - FutureBuilder( - future: room.client.getRoomVisibilityOnDirectory(room.id), - builder: (context, snapshot) => SwitchListTile.adaptive( - value: snapshot.data == Visibility.public, + ListTile( + title: Text( + L10n.of(context)!.whoIsAllowedToJoinThisGroup, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + ), + for (final joinRule in JoinRules.values) + if (joinRule != JoinRules.private) + RadioListTile.adaptive( + title: Text( + joinRule.localizedString(L10n.of(context)!), + ), + value: joinRule, + groupValue: room.joinRules, + onChanged: controller.joinRulesLoading || + !room.canChangeJoinRules + ? null + : controller.setJoinRule, + ), + Divider(color: Theme.of(context).dividerColor), + if ({JoinRules.public, JoinRules.knock} + .contains(room.joinRules)) ...[ + ListTile( title: Text( - L10n.of(context)!.chatCanBeDiscoveredViaSearchOnServer( - room.client.userID!.domain!, + L10n.of(context)!.areGuestsAllowedToJoin, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, ), ), - onChanged: controller.setChatVisibilityOnDirectory, + ), + for (final guestAccess in GuestAccess.values) + RadioListTile.adaptive( + title: Text( + guestAccess.getLocalizedString( + MatrixLocals(L10n.of(context)!), + ), + ), + value: guestAccess, + groupValue: room.guestAccess, + onChanged: controller.guestAccessLoading || + !room.canChangeGuestAccess + ? null + : controller.setGuestAccess, + ), + Divider(color: Theme.of(context).dividerColor), + ListTile( + title: Text( + L10n.of(context)!.publicLinks, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + trailing: IconButton( + icon: const Icon(Icons.add_outlined), + tooltip: L10n.of(context)!.createNewLink, + onPressed: controller.addAlias, + ), + ), + if (canonicalAlias.isNotEmpty) + _AliasListTile( + alias: canonicalAlias, + onDelete: room.canChangeStateEvent( + EventTypes.RoomCanonicalAlias, + ) + ? () => controller.deleteAlias(canonicalAlias) + : null, + isCanonicalAlias: true, + ), + for (final alias in altAliases) + _AliasListTile( + alias: alias, + onDelete: room.canChangeStateEvent( + EventTypes.RoomCanonicalAlias, + ) + ? () => controller.deleteAlias(alias) + : null, + ), + Divider(color: Theme.of(context).dividerColor), + FutureBuilder( + future: room.client.getRoomVisibilityOnDirectory(room.id), + builder: (context, snapshot) => SwitchListTile.adaptive( + value: snapshot.data == Visibility.public, + title: Text( + L10n.of(context)!.chatCanBeDiscoveredViaSearchOnServer( + room.client.userID!.domain!, + ), + ), + onChanged: controller.setChatVisibilityOnDirectory, + ), + ), + ], + ListTile( + title: Text(L10n.of(context)!.globalChatId), + subtitle: SelectableText(room.id), + trailing: IconButton( + icon: const Icon(Icons.copy_outlined), + onPressed: () => FluffyShare.share(room.id, context), ), ), ListTile( - title: Text(L10n.of(context)!.publicLink), - subtitle: room.canonicalAlias.isEmpty - ? Text( - L10n.of(context)!.noPublicLinkHasBeenCreatedYet, - style: const TextStyle( - fontStyle: FontStyle.italic, - ), + title: Text(L10n.of(context)!.roomVersion), + subtitle: SelectableText( + room + .getState(EventTypes.RoomCreate)! + .content + .tryGet('room_version') ?? + 'Unknown', + ), + trailing: room.canSendEvent(EventTypes.RoomTombstone) + ? IconButton( + icon: const Icon(Icons.upgrade_outlined), + onPressed: controller.updateRoomAction, ) - : Text( - 'https://matrix.to/#/${room.canonicalAlias}', - style: TextStyle( - decoration: TextDecoration.underline, - color: Theme.of(context).colorScheme.primary, - ), - ), - onTap: room.canChangeStateEvent(EventTypes.RoomCanonicalAlias) - ? controller.setCanonicalAlias : null, - trailing: room.canonicalAlias.isEmpty - ? const Padding( - padding: EdgeInsets.symmetric(horizontal: 8.0), - child: Icon(Icons.add), - ) - : IconButton( - icon: Icon(Icons.adaptive.share_outlined), - onPressed: () => FluffyShare.share( - 'https://matrix.to/#/${room.canonicalAlias}', - context, - ), - ), ), ], - ListTile( - title: Text(L10n.of(context)!.globalChatId), - subtitle: SelectableText(room.id), - trailing: IconButton( - icon: const Icon(Icons.copy_outlined), - onPressed: () => FluffyShare.share(room.id, context), - ), - ), - ListTile( - title: Text(L10n.of(context)!.roomVersion), - subtitle: SelectableText( - room - .getState(EventTypes.RoomCreate)! - .content - .tryGet('room_version') ?? - 'Unknown', - ), - trailing: room.canSendEvent(EventTypes.RoomTombstone) - ? IconButton( - icon: const Icon(Icons.upgrade_outlined), - onPressed: controller.updateRoomAction, - ) - : null, - ), - ], - ), + ); + }, ), ), ); } } +class _AliasListTile extends StatelessWidget { + const _AliasListTile({ + required this.alias, + required this.onDelete, + this.isCanonicalAlias = false, + }); + + final String alias; + final void Function()? onDelete; + final bool isCanonicalAlias; + + @override + Widget build(BuildContext context) { + return ListTile( + title: Row( + children: [ + TextButton.icon( + onPressed: () => FluffyShare.share( + 'https://matrix.to/#/$alias', + context, + ), + icon: isCanonicalAlias + ? const Icon(Icons.star) + : const Icon(Icons.link_outlined), + label: SelectableText( + 'https://matrix.to/#/$alias', + style: TextStyle( + decoration: TextDecoration.underline, + decorationColor: Theme.of(context).colorScheme.primary, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + const Spacer(), + ], + ), + trailing: onDelete != null + ? IconButton( + icon: const Icon(Icons.delete_outlined), + onPressed: onDelete, + ) + : null, + ); + } +} + extension JoinRulesDisplayString on JoinRules { String localizedString(L10n l10n) { switch (this) {