fluffychat merge

This commit is contained in:
ggurdin 2024-06-14 16:15:45 -04:00
commit d7307af58b
15 changed files with 5722 additions and 5314 deletions

View file

@ -2393,7 +2393,7 @@
"seconds": {}
}
},
"hasKnocked": "لقد طرق {user}",
"hasKnocked": "🚪 لقد طرق {user}",
"@hasKnocked": {
"placeholders": {
"user": {}
@ -2618,5 +2618,63 @@
}
},
"noDatabaseEncryption": "تشفير قاعدة البيانات غير مدعوم على هذا النظام الأساسي",
"@noDatabaseEncryption": {}
"@noDatabaseEncryption": {},
"appLockDescription": "قفل التطبيق عند عدم استخدامه بالرمز السري",
"@appLockDescription": {},
"accessAndVisibility": "الوصول والرؤية",
"@accessAndVisibility": {},
"calls": "المكالمات",
"@calls": {},
"customEmojisAndStickers": "الرموز التعبيرية والملصقات المخصصة",
"@customEmojisAndStickers": {},
"hideRedactedMessagesBody": "إذا قام شخص ما بتنقيح رسالة، فلن تكون هذه الرسالة مرئية في الدردشة بعد الآن.",
"@hideRedactedMessagesBody": {},
"hideInvalidOrUnknownMessageFormats": "إخفاء تنسيقات الرسائل غير الصالحة أو غير المعروفة",
"@hideInvalidOrUnknownMessageFormats": {},
"overview": "نظرة عامة",
"@overview": {},
"notifyMeFor": "أعلمني بـ",
"@notifyMeFor": {},
"passwordRecoverySettings": "إعدادات استعادة كلمة المرور",
"@passwordRecoverySettings": {},
"globalChatId": "معرف الدردشة العامة",
"@globalChatId": {},
"accessAndVisibilityDescription": "من المسموح له بالانضمام إلى هذه الدردشة وكيف يمكن اكتشاف الدردشة.",
"@accessAndVisibilityDescription": {},
"customEmojisAndStickersBody": "قم بإضافة أو مشاركة الرموز التعبيرية أو الملصقات المخصصة التي يمكن استخدامها في أي دردشة.",
"@customEmojisAndStickersBody": {},
"hideRedactedMessages": "إخفاء الرسائل المكررة",
"@hideRedactedMessages": {},
"hideMemberChangesInPublicChats": "إخفاء تغييرات الأعضاء في الدردشات العامة",
"@hideMemberChangesInPublicChats": {},
"hideMemberChangesInPublicChatsBody": "لا تظهر في المخطط الزمني للدردشة إذا انضم شخص ما إلى محادثة عامة أو غادرها لتحسين إمكانية القراءة.",
"@hideMemberChangesInPublicChatsBody": {},
"usersMustKnock": "المستخدم يجب أن يطرق الباب",
"@usersMustKnock": {},
"chatCanBeDiscoveredViaSearchOnServer": "يمكن اكتشاف الشات عن طريق البحث في {server}",
"@chatCanBeDiscoveredViaSearchOnServer": {
"type": "text",
"placeholders": {
"server": {}
}
},
"noOneCanJoin": "لا أحد يستطيع الانضمام",
"@noOneCanJoin": {},
"knocking": "طرق",
"@knocking": {},
"userWouldLikeToChangeTheChat": "{user} يرغب في الانضمام إلى الدردشة.",
"@userWouldLikeToChangeTheChat": {
"placeholders": {
"user": {}
}
},
"noPublicLinkHasBeenCreatedYet": "لم يتم بعد إنشاء أي رابط عام",
"@noPublicLinkHasBeenCreatedYet": {},
"knock": "دق",
"@knock": {},
"thereAreCountUsersBlocked": "يوجد حاليًا {count} من المستخدمين المحظورين.",
"@thereAreCountUsersBlocked": {
"type": "text",
"count": {}
}
}

View file

@ -3792,6 +3792,9 @@
"passwordsDoNotMatch": "Passwords do not match",
"passwordIsWrong": "Your entered password is wrong",
"publicLink": "Public link",
"@publicLink": {},
"publicChatAddresses": "Public chat addresses",
"createNewAddress": "Create new address",
"joinSpace": "Join space",
"publicSpaces": "Public spaces",
"addChatOrSubSpace": "Add chat or sub space",
@ -3799,6 +3802,15 @@
"decline": "Decline",
"thisDevice": "This device:",
"initAppError": "An error occured while init the app",
"@initAppError": {},
"userRole": "User role",
"minimumPowerLevel": "{level} is the minimum power level.",
"@minimumPowerLevel": {
"type": "text",
"placeholders": {
"level": {}
}
},
"databaseBuildErrorBody": "Unable to build the SQlite database. The app tries to use the legacy database for now. Please report this error to the developers at {url}. The error message is: {error}",
"@databaseBuildErrorBody": {
"type": "text",

View file

@ -2405,7 +2405,7 @@
"@makeAdminDescription": {},
"archiveRoomDescription": "Txata artxibategira mugituko da. Beste erabiltzaileek txatetik alde egin duzula ikusi ahal izango dute.",
"@archiveRoomDescription": {},
"hasKnocked": "{user}(e)k baimena eskatu du",
"hasKnocked": "🚪 {user}(e)k baimena eskatu du",
"@hasKnocked": {
"placeholders": {
"user": {}
@ -2618,5 +2618,63 @@
}
},
"noDatabaseEncryption": "Plataforma honetan ezin da datu-basea zifratu",
"@noDatabaseEncryption": {}
"@noDatabaseEncryption": {},
"usersMustKnock": "Erabiltzaileek baimena eskatu behar dute",
"@usersMustKnock": {},
"userWouldLikeToChangeTheChat": "{user}(e)k txatera batu nahiko luke.",
"@userWouldLikeToChangeTheChat": {
"placeholders": {
"user": {}
}
},
"knock": "Eskatu baimena",
"@knock": {},
"knocking": "Baimena eskatzen",
"@knocking": {},
"chatCanBeDiscoveredViaSearchOnServer": "Txata {server}(e)n bilaketa eginez aurkitu daiteke",
"@chatCanBeDiscoveredViaSearchOnServer": {
"type": "text",
"placeholders": {
"server": {}
}
},
"thereAreCountUsersBlocked": "Une honetan {count} erabiltzaile daude blokeatuta.",
"@thereAreCountUsersBlocked": {
"type": "text",
"count": {}
},
"appLockDescription": "Blokeatu aplikazioa pin kode batekin erabiltzen ari ez zarenean",
"@appLockDescription": {},
"accessAndVisibility": "Sarbidea eta ikusgaitasuna",
"@accessAndVisibility": {},
"accessAndVisibilityDescription": "Nork du txat honetara batzeko baimena eta nola aurkitu daiteke txata.",
"@accessAndVisibilityDescription": {},
"customEmojisAndStickers": "Emoji eta pegatina propioak",
"@customEmojisAndStickers": {},
"customEmojisAndStickersBody": "Gehitu edo partekatu edozein txatetan erabil daitezkeen emoji edo pegatina propioak.",
"@customEmojisAndStickersBody": {},
"hideRedactedMessages": "Ezkutatu atzera botatako mezuak",
"@hideRedactedMessages": {},
"hideRedactedMessagesBody": "Norbaitek mezuren bat atzera botaz gero, mezua txatetik kenduko da, abisurik gabe.",
"@hideRedactedMessagesBody": {},
"hideInvalidOrUnknownMessageFormats": "Ezkutatu mezuen formatu ezezagun edo baliogabea",
"@hideInvalidOrUnknownMessageFormats": {},
"overview": "Ikuspegi orokorra",
"@overview": {},
"notifyMeFor": "Jakinarazi…",
"@notifyMeFor": {},
"passwordRecoverySettings": "Pasahitza berreskuratzeko ezarpenak",
"@passwordRecoverySettings": {},
"hideMemberChangesInPublicChats": "Ezkutatu kideen egoera aldaketak txat publikoetan",
"@hideMemberChangesInPublicChats": {},
"globalChatId": "Txat ID orokorra",
"@globalChatId": {},
"calls": "Deiak",
"@calls": {},
"hideMemberChangesInPublicChatsBody": "Ez erakutsi txataren denbora-lerroan norbait txat publikora batu edo txatetik irteten dela, irakurgaitasuna hobetzeko.",
"@hideMemberChangesInPublicChatsBody": {},
"noOneCanJoin": "Ezin da inor batu",
"@noOneCanJoin": {},
"noPublicLinkHasBeenCreatedYet": "Oraindik ez da esteka publikorik sortu",
"@noPublicLinkHasBeenCreatedYet": {}
}

File diff suppressed because it is too large Load diff

View file

@ -2404,7 +2404,7 @@
"@makeAdminDescription": {},
"archiveRoomDescription": "Percakapan akan dipindahkan ke arsip. Pengguna lain akan melihat bahwa kamu telah meninggalkan percakapan.",
"@archiveRoomDescription": {},
"hasKnocked": "{user} telah dikeluarkan",
"hasKnocked": "🚪 {user} telah dikeluarkan",
"@hasKnocked": {
"placeholders": {
"user": {}
@ -2617,5 +2617,63 @@
}
},
"noDatabaseEncryption": "Enkripsi basis data tidak didukung di platform ini",
"@noDatabaseEncryption": {}
"@noDatabaseEncryption": {},
"customEmojisAndStickersBody": "Tambakan atau bagikan emoji atau stiker kustom yang dapat digunakan dalam obrolan apa pun.",
"@customEmojisAndStickersBody": {},
"hideRedactedMessages": "Sembunyikan pesan yang dihapus",
"@hideRedactedMessages": {},
"appLockDescription": "Kunci aplikasi ketika tidak digunakan dengan kode PIN",
"@appLockDescription": {},
"accessAndVisibility": "Akses dan keterlihatan",
"@accessAndVisibility": {},
"globalChatId": "ID obrolan global",
"@globalChatId": {},
"accessAndVisibilityDescription": "Siapa yang diperbolehkan bergabung ke obrolan ini dan bagaimana obrolannya dapat ditemukan.",
"@accessAndVisibilityDescription": {},
"calls": "Panggilan",
"@calls": {},
"customEmojisAndStickers": "Emoji dan stiker kustom",
"@customEmojisAndStickers": {},
"hideRedactedMessagesBody": "Jika seseorang menghapus pesan, pesannya tidak akan terlihat lagi dalam obrolan.",
"@hideRedactedMessagesBody": {},
"hideMemberChangesInPublicChatsBody": "Jangan tampilkan dalam lini masa obrolan jika seseorang bergabung atau keluar dari obrolan untuk meningkatkan keterlihatan.",
"@hideMemberChangesInPublicChatsBody": {},
"notifyMeFor": "Beri tahu aku untuk",
"@notifyMeFor": {},
"hideInvalidOrUnknownMessageFormats": "Sembunyikan format pesan yang tidak valid atau tidak diketahui",
"@hideInvalidOrUnknownMessageFormats": {},
"hideMemberChangesInPublicChats": "Sembunyikan perubahan anggota dalam obrolan publik",
"@hideMemberChangesInPublicChats": {},
"overview": "Ikhtisar",
"@overview": {},
"passwordRecoverySettings": "Pengaturan pemulihan kata sandi",
"@passwordRecoverySettings": {},
"usersMustKnock": "Pengguna harus mengetuk",
"@usersMustKnock": {},
"noOneCanJoin": "Tidak ada siapa pun yang dapat bergabung",
"@noOneCanJoin": {},
"userWouldLikeToChangeTheChat": "{user} ingin bergabung dengan obrolan.",
"@userWouldLikeToChangeTheChat": {
"placeholders": {
"user": {}
}
},
"noPublicLinkHasBeenCreatedYet": "Belum ada tautan publik yang dibuat",
"@noPublicLinkHasBeenCreatedYet": {},
"knock": "Ketuk",
"@knock": {},
"knocking": "Mengetuk",
"@knocking": {},
"chatCanBeDiscoveredViaSearchOnServer": "Obrolan dapat ditemukan melalui pencarian di {server}",
"@chatCanBeDiscoveredViaSearchOnServer": {
"type": "text",
"placeholders": {
"server": {}
}
},
"thereAreCountUsersBlocked": "Saat ini ada {count} pengguna yang diblokir.",
"@thereAreCountUsersBlocked": {
"type": "text",
"count": {}
}
}

File diff suppressed because it is too large Load diff

View file

@ -2616,5 +2616,65 @@
"appname": {},
"unread": {}
}
}
},
"appLockDescription": "用 pin 码在不用 FluffyChat 时锁定它",
"@appLockDescription": {},
"globalChatId": "全局聊天 ID",
"@globalChatId": {},
"accessAndVisibility": "访问和可见性",
"@accessAndVisibility": {},
"accessAndVisibilityDescription": "谁可以加入此聊天以及怎样发现该聊天。",
"@accessAndVisibilityDescription": {},
"calls": "通话",
"@calls": {},
"customEmojisAndStickers": "自定义表情符号和贴纸",
"@customEmojisAndStickers": {},
"hideRedactedMessages": "隐藏被涂黑的消息",
"@hideRedactedMessages": {},
"overview": "概览",
"@overview": {},
"notifyMeFor": "提示内容",
"@notifyMeFor": {},
"passwordRecoverySettings": "密码发现设置",
"@passwordRecoverySettings": {},
"noPublicLinkHasBeenCreatedYet": "尚未创建公开链接",
"@noPublicLinkHasBeenCreatedYet": {},
"knock": "请求",
"@knock": {},
"noOneCanJoin": "无人可以加入",
"@noOneCanJoin": {},
"knocking": "正在请求",
"@knocking": {},
"chatCanBeDiscoveredViaSearchOnServer": "可通过搜索 {server} 发现聊天",
"@chatCanBeDiscoveredViaSearchOnServer": {
"type": "text",
"placeholders": {
"server": {}
}
},
"thereAreCountUsersBlocked": "目前有 {count} 名用户被封禁。",
"@thereAreCountUsersBlocked": {
"type": "text",
"count": {}
},
"hideRedactedMessagesBody": "如果某人涂黑了一条消息,那么在聊天中再也看不到这条消息。",
"@hideRedactedMessagesBody": {},
"hideInvalidOrUnknownMessageFormats": "隐藏无效或未知的消息格式",
"@hideInvalidOrUnknownMessageFormats": {},
"hideMemberChangesInPublicChats": "在公开聊天中隐藏成员变化",
"@hideMemberChangesInPublicChats": {},
"hideMemberChangesInPublicChatsBody": "不在聊天时间线中显示某人是否加入或离开了公开聊天来改进可读性。",
"@hideMemberChangesInPublicChatsBody": {},
"userWouldLikeToChangeTheChat": "{user} 想加入聊天。",
"@userWouldLikeToChangeTheChat": {
"placeholders": {
"user": {}
}
},
"customEmojisAndStickersBody": "添加或分享可用于任何聊天的表情符号或贴纸。",
"@customEmojisAndStickersBody": {},
"usersMustKnock": "用户必须请求加入",
"@usersMustKnock": {},
"noDatabaseEncryption": "数据库加密在此平台上不受支持",
"@noDatabaseEncryption": {}
}

View file

@ -152,31 +152,82 @@ class ChatAccessSettingsController extends State<ChatAccessSettings> {
);
}
void setCanonicalAlias() async {
Future<void> 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;
setState(() {});
if (!room.canChangeStateEvent(EventTypes.RoomCanonicalAlias)) 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<String>('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) async {
await showFutureLoadingDialog(
context: context,
future: () => room.client.deleteRoomAlias(alias),
);
setState(() {});
}
void setChatVisibilityOnDirectory(bool? visibility) async {
if (visibility == null) return;
setState(() {

View file

@ -23,156 +23,240 @@ class ChatAccessSettingsPageView extends StatelessWidget {
body: MaxWidthBody(
child: StreamBuilder<Object>(
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<HistoryVisibility>.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)
RadioListTile<JoinRules>.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<String>('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<GuestAccess>.adaptive(
for (final historyVisibility in HistoryVisibility.values)
RadioListTile<HistoryVisibility>.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<JoinRules>.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<GuestAccess>.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)!.publicChatAddresses,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
),
),
trailing: IconButton(
icon: const Icon(Icons.add_outlined),
tooltip: L10n.of(context)!.createNewAddress,
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,
),
FutureBuilder(
future: room.client.getLocalAliases(room.id),
builder: (context, snapshot) {
final localAddresses = snapshot.data;
if (localAddresses == null) {
return const SizedBox.shrink();
}
localAddresses.remove(room.canonicalAlias);
localAddresses
.removeWhere((alias) => altAliases.contains(alias));
return Column(
mainAxisSize: MainAxisSize.min,
children: localAddresses
.map(
(alias) => _AliasListTile(
alias: alias,
published: false,
onDelete: () => controller.deleteAlias(alias),
),
)
.toList(),
);
},
),
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<String>('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<String>('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,
this.published = true,
});
final String alias;
final void Function()? onDelete;
final bool isCanonicalAlias;
final bool published;
@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) {

View file

@ -25,6 +25,7 @@ class ChatPermissionsSettingsController extends State<ChatPermissionsSettings> {
BuildContext context,
String key,
int currentLevel, {
int? newLevel,
String? category,
}) async {
final room = Matrix.of(context).client.getRoomById(roomId!)!;
@ -34,7 +35,7 @@ class ChatPermissionsSettingsController extends State<ChatPermissionsSettings> {
);
return;
}
final newLevel = await showPermissionChooser(
newLevel ??= await showPermissionChooser(
context,
currentLevel: currentLevel,
);

View file

@ -48,11 +48,13 @@ class ChatPermissionsSettingsView extends StatelessWidget {
PermissionsListTile(
permissionKey: entry.key,
permission: entry.value,
onTap: () => controller.editPowerLevel(
onChanged: (level) => controller.editPowerLevel(
context,
entry.key,
entry.value,
newLevel: level,
),
canEdit: room.canChangePowerLevel,
),
Divider(color: Theme.of(context).dividerColor),
ListTile(
@ -78,10 +80,12 @@ class ChatPermissionsSettingsView extends StatelessWidget {
permissionKey: key,
permission: value,
category: 'notifications',
onTap: () => controller.editPowerLevel(
canEdit: room.canChangePowerLevel,
onChanged: (level) => controller.editPowerLevel(
context,
key,
value,
newLevel: level,
category: 'notifications',
),
);
@ -102,10 +106,12 @@ class ChatPermissionsSettingsView extends StatelessWidget {
permissionKey: entry.key,
category: 'events',
permission: entry.value ?? 0,
onTap: () => controller.editPowerLevel(
canEdit: room.canChangePowerLevel,
onChanged: (level) => controller.editPowerLevel(
context,
entry.key,
entry.value ?? 0,
newLevel: level,
category: 'events',
),
),

View file

@ -3,18 +3,22 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
class PermissionsListTile extends StatelessWidget {
final String permissionKey;
final int permission;
final String? category;
final void Function()? onTap;
final void Function(int? level)? onChanged;
final bool canEdit;
const PermissionsListTile({
super.key,
required this.permissionKey,
required this.permission,
this.category,
this.onTap,
required this.onChanged,
required this.canEdit,
});
String getLocalizedPowerLevelString(BuildContext context) {
@ -66,39 +70,39 @@ class PermissionsListTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListTile(
onTap: onTap,
leading: CircleAvatar(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
foregroundColor: Colors.grey,
child: const Icon(Icons.edit_attributes_outlined),
),
title: Text(getLocalizedPowerLevelString(context)),
subtitle: Row(
children: [
Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Theme.of(context).secondaryHeaderColor,
borderRadius: BorderRadius.circular(8),
subtitle: Text(
L10n.of(context)!.minimumPowerLevel(permission.toString()),
),
trailing: Material(
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
color: Theme.of(context).colorScheme.onInverseSurface,
child: DropdownButton<int>(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
underline: const SizedBox.shrink(),
onChanged: canEdit ? onChanged : null,
value: {0, 50, 100}.contains(permission) ? permission : null,
items: [
DropdownMenuItem(
value: 0,
child: Text(L10n.of(context)!.user),
),
child: Center(
child: Text(permission.toString()),
DropdownMenuItem(
value: 50,
child: Text(L10n.of(context)!.moderator),
),
),
const SizedBox(width: 8),
Text(permission.toLocalizedPowerLevelString(context)),
],
DropdownMenuItem(
value: 100,
child: Text(L10n.of(context)!.admin),
),
DropdownMenuItem(
value: null,
child: Text(L10n.of(context)!.custom),
),
],
),
),
);
}
}
extension on int {
String toLocalizedPowerLevelString(BuildContext context) {
return this == 100
? L10n.of(context)!.admin
: this >= 50
? L10n.of(context)!.moderator
: L10n.of(context)!.participant;
}
}

View file

@ -17,7 +17,6 @@ enum UserBottomSheetAction {
ban,
kick,
unban,
permission,
message,
ignore,
}
@ -208,30 +207,6 @@ class UserBottomSheetController extends State<UserBottomSheet> {
Navigator.of(context).pop();
}
break;
case UserBottomSheetAction.permission:
if (user == null) throw ('User must not be null for this action!');
final newPermission = await showPermissionChooser(
context,
currentLevel: user.powerLevel,
);
if (newPermission != null) {
if (newPermission == 100 &&
await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context)!.areYouSure,
okLabel: L10n.of(context)!.yes,
cancelLabel: L10n.of(context)!.no,
message: L10n.of(context)!.makeAdminDescription,
) !=
OkCancelResult.ok) break;
await showFutureLoadingDialog(
context: context,
future: () => user.setPower(newPermission),
);
Navigator.of(context).pop();
}
break;
case UserBottomSheetAction.message:
Navigator.of(context).pop();
// Workaround for https://github.com/flutter/flutter/issues/27495
@ -280,6 +255,35 @@ class UserBottomSheetController extends State<UserBottomSheet> {
Navigator.of(context).pop();
}
void setPowerLevel(int? newLevel) async {
final user = widget.user;
if (user == null) throw ('User must not be null for this action!');
final level = newLevel ??
await showPermissionChooser(
context,
currentLevel: user.powerLevel,
);
if (level == null) return;
if (level == 100) {
final consent = await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context)!.areYouSure,
okLabel: L10n.of(context)!.yes,
cancelLabel: L10n.of(context)!.no,
message: L10n.of(context)!.makeAdminDescription,
);
if (consent != OkCancelResult.ok) return;
}
await showFutureLoadingDialog(
context: context,
future: () => user.setPower(level),
);
}
@override
Widget build(BuildContext context) => UserBottomSheetView(this);
}

View file

@ -104,228 +104,286 @@ class UserBottomSheetView extends StatelessWidget {
),
],
),
body: ListView(
children: [
if (user?.membership == Membership.knock)
Padding(
padding: const EdgeInsets.all(12.0),
child: Material(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
child: ListTile(
minVerticalPadding: 16,
title: Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Text(
L10n.of(context)!
.userWouldLikeToChangeTheChat(displayname),
),
),
subtitle: Row(
children: [
TextButton.icon(
style: TextButton.styleFrom(
backgroundColor:
Theme.of(context).colorScheme.background,
foregroundColor:
Theme.of(context).colorScheme.primary,
),
onPressed: controller.knockAccept,
icon: const Icon(Icons.check_outlined),
label: Text(L10n.of(context)!.accept),
),
const SizedBox(width: 12),
TextButton.icon(
style: TextButton.styleFrom(
backgroundColor:
Theme.of(context).colorScheme.errorContainer,
foregroundColor:
Theme.of(context).colorScheme.onErrorContainer,
),
onPressed: controller.knockDecline,
icon: const Icon(Icons.cancel_outlined),
label: Text(L10n.of(context)!.decline),
),
],
),
),
),
),
Row(
body: StreamBuilder<Object>(
stream: user?.room.client.onSync.stream.where(
(syncUpdate) =>
syncUpdate.rooms?.join?[user.room.id]?.timeline?.events?.any(
(state) => state.type == EventTypes.RoomPowerLevels,
) ??
false,
),
builder: (context, snapshot) {
return ListView(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Material(
elevation:
Theme.of(context).appBarTheme.scrolledUnderElevation ??
if (user?.membership == Membership.knock)
Padding(
padding: const EdgeInsets.all(12.0),
child: Material(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius:
BorderRadius.circular(AppConfig.borderRadius),
child: ListTile(
minVerticalPadding: 16,
title: Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Text(
L10n.of(context)!
.userWouldLikeToChangeTheChat(displayname),
),
),
subtitle: Row(
children: [
TextButton.icon(
style: TextButton.styleFrom(
backgroundColor:
Theme.of(context).colorScheme.background,
foregroundColor:
Theme.of(context).colorScheme.primary,
),
onPressed: controller.knockAccept,
icon: const Icon(Icons.check_outlined),
label: Text(L10n.of(context)!.accept),
),
const SizedBox(width: 12),
TextButton.icon(
style: TextButton.styleFrom(
backgroundColor: Theme.of(context)
.colorScheme
.errorContainer,
foregroundColor: Theme.of(context)
.colorScheme
.onErrorContainer,
),
onPressed: controller.knockDecline,
icon: const Icon(Icons.cancel_outlined),
label: Text(L10n.of(context)!.decline),
),
],
),
),
),
),
Row(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Material(
elevation: Theme.of(context)
.appBarTheme
.scrolledUnderElevation ??
4,
shadowColor: Theme.of(context).appBarTheme.shadowColor,
shape: RoundedRectangleBorder(
side: BorderSide(
color: Theme.of(context).dividerColor,
),
borderRadius: BorderRadius.circular(
Avatar.defaultSize * 2.5,
shadowColor: Theme.of(context).appBarTheme.shadowColor,
shape: RoundedRectangleBorder(
side: BorderSide(
color: Theme.of(context).dividerColor,
),
borderRadius: BorderRadius.circular(
Avatar.defaultSize * 2.5,
),
),
child: Avatar(
mxContent: avatarUrl,
name: displayname,
size: Avatar.defaultSize * 2.5,
fontSize: 18 * 2.5,
),
),
),
child: Avatar(
mxContent: avatarUrl,
name: displayname,
size: Avatar.defaultSize * 2.5,
fontSize: 18 * 2.5,
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextButton.icon(
onPressed: () => FluffyShare.share(
'https://matrix.to/#/$userId',
context,
),
icon: Icon(
Icons.adaptive.share_outlined,
size: 16,
),
style: TextButton.styleFrom(
foregroundColor:
Theme.of(context).colorScheme.onBackground,
),
label: Text(
displayname,
maxLines: 1,
overflow: TextOverflow.ellipsis,
// style: const TextStyle(fontSize: 18),
),
),
TextButton.icon(
onPressed: () => FluffyShare.share(
userId,
context,
copyOnly: true,
),
icon: const Icon(
Icons.copy_outlined,
size: 14,
),
style: TextButton.styleFrom(
foregroundColor:
Theme.of(context).colorScheme.secondary,
),
label: Text(
userId,
maxLines: 1,
overflow: TextOverflow.ellipsis,
// style: const TextStyle(fontSize: 12),
),
),
],
),
),
],
),
if (userId != client.userID)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: ElevatedButton.icon(
onPressed: () => controller
.participantAction(UserBottomSheetAction.message),
icon: const Icon(Icons.forum_outlined),
label: Text(
controller.widget.user == null
? L10n.of(context)!.startConversation
: L10n.of(context)!.sendAMessage,
),
),
),
PresenceBuilder(
userId: userId,
client: client,
builder: (context, presence) {
final status = presence?.statusMsg;
if (status == null || status.isEmpty) {
return const SizedBox.shrink();
}
return ListTile(
title: SelectableLinkify(
text: status,
style: const TextStyle(fontSize: 16),
options: const LinkifyOptions(humanize: false),
linkStyle: const TextStyle(
color: Colors.blueAccent,
decorationColor: Colors.blueAccent,
),
onOpen: (url) =>
UrlLauncher(context, url.url).launchUrl(),
),
);
},
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextButton.icon(
onPressed: () => FluffyShare.share(
'https://matrix.to/#/$userId',
context,
),
icon: Icon(
Icons.adaptive.share_outlined,
size: 16,
),
style: TextButton.styleFrom(
foregroundColor:
Theme.of(context).colorScheme.onBackground,
),
label: Text(
displayname,
maxLines: 1,
overflow: TextOverflow.ellipsis,
// style: const TextStyle(fontSize: 18),
),
),
TextButton.icon(
onPressed: () => FluffyShare.share(
userId,
context,
copyOnly: true,
),
icon: const Icon(
Icons.copy_outlined,
size: 14,
),
style: TextButton.styleFrom(
foregroundColor:
Theme.of(context).colorScheme.secondary,
),
label: Text(
userId,
maxLines: 1,
overflow: TextOverflow.ellipsis,
// style: const TextStyle(fontSize: 12),
),
),
],
if (controller.widget.onMention != null)
ListTile(
leading: const Icon(Icons.alternate_email_outlined),
title: Text(L10n.of(context)!.mention),
onTap: () => controller
.participantAction(UserBottomSheetAction.mention),
),
if (user != null) ...[
Divider(color: Theme.of(context).dividerColor),
ListTile(
title: Text(
'${L10n.of(context)!.userRole} (${user.powerLevel})',
),
leading: const Icon(Icons.person_outlined),
trailing: Material(
borderRadius:
BorderRadius.circular(AppConfig.borderRadius / 2),
color: Theme.of(context).colorScheme.onInverseSurface,
child: DropdownButton<int>(
onChanged: user.canChangePowerLevel
? controller.setPowerLevel
: null,
value: {0, 50, 100}.contains(user.powerLevel)
? user.powerLevel
: null,
padding: const EdgeInsets.symmetric(horizontal: 8.0),
borderRadius:
BorderRadius.circular(AppConfig.borderRadius / 2),
underline: const SizedBox.shrink(),
items: [
DropdownMenuItem(
value: 0,
child: Text(L10n.of(context)!.user),
),
DropdownMenuItem(
value: 50,
child: Text(L10n.of(context)!.moderator),
),
DropdownMenuItem(
value: 100,
child: Text(L10n.of(context)!.admin),
),
DropdownMenuItem(
value: null,
child: Text(L10n.of(context)!.custom),
),
],
),
),
),
Divider(color: Theme.of(context).dividerColor),
],
if (user != null && user.canKick)
ListTile(
textColor: Theme.of(context).colorScheme.error,
iconColor: Theme.of(context).colorScheme.error,
title: Text(L10n.of(context)!.kickFromChat),
leading: const Icon(Icons.exit_to_app_outlined),
onTap: () => controller
.participantAction(UserBottomSheetAction.kick),
),
if (user != null &&
user.canBan &&
user.membership != Membership.ban)
ListTile(
textColor: Theme.of(context).colorScheme.onErrorContainer,
iconColor: Theme.of(context).colorScheme.onErrorContainer,
title: Text(L10n.of(context)!.banFromChat),
leading: const Icon(Icons.warning_sharp),
onTap: () =>
controller.participantAction(UserBottomSheetAction.ban),
)
else if (user != null &&
user.canBan &&
user.membership == Membership.ban)
ListTile(
title: Text(L10n.of(context)!.unbanFromChat),
leading: const Icon(Icons.warning_outlined),
onTap: () => controller
.participantAction(UserBottomSheetAction.unban),
),
if (user != null && user.id != client.userID)
ListTile(
textColor: Theme.of(context).colorScheme.onErrorContainer,
iconColor: Theme.of(context).colorScheme.onErrorContainer,
title: Text(L10n.of(context)!.reportUser),
leading: const Icon(Icons.report_outlined),
onTap: () => controller
.participantAction(UserBottomSheetAction.report),
),
if (profileSearchError != null)
ListTile(
leading: const Icon(
Icons.warning_outlined,
color: Colors.orange,
),
subtitle: Text(
L10n.of(context)!.profileNotFound,
style: const TextStyle(color: Colors.orange),
),
),
),
],
),
if (userId != client.userID)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: ElevatedButton.icon(
onPressed: () => controller
.participantAction(UserBottomSheetAction.message),
icon: const Icon(Icons.forum_outlined),
label: Text(
controller.widget.user == null
? L10n.of(context)!.startConversation
: L10n.of(context)!.sendAMessage,
),
),
),
PresenceBuilder(
userId: userId,
client: client,
builder: (context, presence) {
final status = presence?.statusMsg;
if (status == null || status.isEmpty) {
return const SizedBox.shrink();
}
return ListTile(
title: SelectableLinkify(
text: status,
style: const TextStyle(fontSize: 16),
options: const LinkifyOptions(humanize: false),
linkStyle: const TextStyle(
color: Colors.blueAccent,
decorationColor: Colors.blueAccent,
),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
),
);
},
),
if (controller.widget.onMention != null)
ListTile(
leading: const Icon(Icons.alternate_email_outlined),
title: Text(L10n.of(context)!.mention),
onTap: () =>
controller.participantAction(UserBottomSheetAction.mention),
),
if (user != null && user.canChangePowerLevel)
ListTile(
title: Text(L10n.of(context)!.setPermissionsLevel),
leading: const Icon(Icons.edit_attributes_outlined),
onTap: () => controller
.participantAction(UserBottomSheetAction.permission),
),
if (user != null && user.canKick)
ListTile(
title: Text(L10n.of(context)!.kickFromChat),
leading: const Icon(Icons.exit_to_app_outlined),
onTap: () =>
controller.participantAction(UserBottomSheetAction.kick),
),
if (user != null &&
user.canBan &&
user.membership != Membership.ban)
ListTile(
title: Text(L10n.of(context)!.banFromChat),
leading: const Icon(Icons.warning_sharp),
onTap: () =>
controller.participantAction(UserBottomSheetAction.ban),
)
else if (user != null &&
user.canBan &&
user.membership == Membership.ban)
ListTile(
title: Text(L10n.of(context)!.unbanFromChat),
leading: const Icon(Icons.warning_outlined),
onTap: () =>
controller.participantAction(UserBottomSheetAction.unban),
),
if (user != null && user.id != client.userID)
ListTile(
textColor: Theme.of(context).colorScheme.onErrorContainer,
iconColor: Theme.of(context).colorScheme.onErrorContainer,
title: Text(L10n.of(context)!.reportUser),
leading: const Icon(Icons.report_outlined),
onTap: () =>
controller.participantAction(UserBottomSheetAction.report),
),
if (profileSearchError != null)
ListTile(
leading: const Icon(
Icons.warning_outlined,
color: Colors.orange,
),
subtitle: Text(
L10n.of(context)!.profileNotFound,
style: const TextStyle(color: Colors.orange),
),
),
],
);
},
),
),
);

View file

@ -4,77 +4,31 @@ import 'package:flutter/material.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
enum PermissionLevel {
user,
moderator,
admin,
custom,
}
extension on PermissionLevel {
String toLocalizedString(BuildContext context) {
switch (this) {
case PermissionLevel.user:
return L10n.of(context)!.user;
case PermissionLevel.moderator:
return L10n.of(context)!.moderator;
case PermissionLevel.admin:
return L10n.of(context)!.admin;
case PermissionLevel.custom:
default:
return L10n.of(context)!.custom;
}
}
}
Future<int?> showPermissionChooser(
BuildContext context, {
int currentLevel = 0,
}) async {
final permissionLevel = await showConfirmationDialog(
final customLevel = await showTextInputDialog(
context: context,
title: L10n.of(context)!.setPermissionsLevel,
actions: PermissionLevel.values
.map(
(level) => AlertDialogAction(
key: level,
label: level.toLocalizedString(context),
),
)
.toList(),
textFields: [
DialogTextField(
initialText: currentLevel.toString(),
keyboardType: TextInputType.number,
autocorrect: false,
validator: (text) {
if (text == null) {
return L10n.of(context)!.pleaseEnterANumber;
}
final level = int.tryParse(text);
if (level == null || level < 0) {
return L10n.of(context)!.pleaseEnterANumber;
}
return null;
},
),
],
);
if (permissionLevel == null) return null;
switch (permissionLevel) {
case PermissionLevel.user:
return 0;
case PermissionLevel.moderator:
return 50;
case PermissionLevel.admin:
return 100;
case PermissionLevel.custom:
final customLevel = await showTextInputDialog(
context: context,
title: L10n.of(context)!.setPermissionsLevel,
textFields: [
DialogTextField(
initialText: currentLevel.toString(),
keyboardType: TextInputType.number,
autocorrect: false,
validator: (text) {
if (text == null) {
return L10n.of(context)!.pleaseEnterANumber;
}
final level = int.tryParse(text);
if (level == null || level < 0) {
return L10n.of(context)!.pleaseEnterANumber;
}
return null;
},
),
],
);
if (customLevel == null) return null;
return int.tryParse(customLevel.first);
}
if (customLevel == null) return null;
return int.tryParse(customLevel.first);
}