From f2e609ad13a1c49f04e3f7e4d35a508187c8043a Mon Sep 17 00:00:00 2001 From: Tim Segers Date: Thu, 8 Oct 2020 11:44:35 +0000 Subject: [PATCH 01/20] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d1d9449..a19f67b 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@
- Open FluffyChat in the browser - Join the community - Follow me on Mastodon - Translate FluffyChat - Translate the website - FAQ - Website - Download latest APK + Open FluffyChat in the browser - Join the community - Follow me on Mastodon - Translate FluffyChat - Translate the website - FAQ - Website - Download latest APK - Famedly Matrix SDK



From 060156ce12fa0a36c9bbd4c596c7813f5aa39259 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kate=C5=99ina=20Churanov=C3=A1?= Date: Sun, 11 Oct 2020 13:25:06 +0200 Subject: [PATCH 02/20] fix: fixed mxid input method, removed code redundancy --- lib/views/homeserver_picker.dart | 6 ------ lib/views/login.dart | 1 - 2 files changed, 7 deletions(-) diff --git a/lib/views/homeserver_picker.dart b/lib/views/homeserver_picker.dart index b1bc842..a7d7b43 100644 --- a/lib/views/homeserver_picker.dart +++ b/lib/views/homeserver_picker.dart @@ -30,12 +30,6 @@ class HomeserverPicker extends StatelessWidget { homeserver = 'https://$homeserver'; } - // removes trailing spaces and slash from url if present (api errors on it) - homeserver = homeserver.trim(); - if (homeserver.endsWith('/')) { - homeserver = homeserver.substring(0, homeserver.length - 1); - } - final success = await SimpleDialogs(context).tryRequestWithLoadingDialog( Matrix.of(context).client.checkServer(homeserver)); if (success != false) { diff --git a/lib/views/login.dart b/lib/views/login.dart index 40903c2..d7d327a 100644 --- a/lib/views/login.dart +++ b/lib/views/login.dart @@ -131,7 +131,6 @@ class _LoginState extends State { readOnly: loading, autocorrect: false, autofocus: true, - keyboardType: TextInputType.emailAddress, onChanged: (t) => _checkWellKnownWithCoolDown(t, context), controller: usernameController, decoration: InputDecoration( From 39d8b7b09f4f18a170d11b5c5992b90e409ab561 Mon Sep 17 00:00:00 2001 From: abidin toumi Date: Sat, 10 Oct 2020 20:04:13 +0000 Subject: [PATCH 03/20] Translated using Weblate (Arabic) Currently translated at 90.7% (284 of 313 strings) Translation: FluffyChat/Translations-New Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations-new/ar/ --- lib/l10n/intl_ar.arb | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/lib/l10n/intl_ar.arb b/lib/l10n/intl_ar.arb index e46adbd..f21d888 100644 --- a/lib/l10n/intl_ar.arb +++ b/lib/l10n/intl_ar.arb @@ -189,7 +189,7 @@ "username": {} } }, - "changedTheDisplaynameTo": "{username} غيّر اسمه الى {displayname}", + "changedTheDisplaynameTo": "{username} غيّر اسمه العلني الى {displayname}", "@changedTheDisplaynameTo": { "type": "text", "placeholders": { @@ -375,7 +375,7 @@ "type": "text", "placeholders": {} }, - "couldNotSetDisplayname": "تعذر تعيين الاسم", + "couldNotSetDisplayname": "تعذر تعيين الاسم العلني", "@couldNotSetDisplayname": { "type": "text", "placeholders": {} @@ -489,7 +489,7 @@ "type": "text", "placeholders": {} }, - "displaynameHasBeenChanged": "غُيِّر الاسم", + "displaynameHasBeenChanged": "غُيِّر الاسم العلني", "@displaynameHasBeenChanged": { "type": "text", "placeholders": {} @@ -499,7 +499,7 @@ "type": "text", "placeholders": {} }, - "editDisplayname": "حرر الاسم", + "editDisplayname": "حرر الاسم العلني", "@editDisplayname": { "type": "text", "placeholders": {} @@ -514,7 +514,7 @@ "type": "text", "placeholders": {} }, - "emoteWarnNeedToPick": "اختر صورة ورمزا للانفعالة", + "emoteWarnNeedToPick": "اختر صورة ورمزا للانفعالة!", "@emoteWarnNeedToPick": { "type": "text", "placeholders": {} @@ -1374,7 +1374,7 @@ "type": "text", "placeholders": {} }, - "useAmoledTheme": "", + "useAmoledTheme": "هل تريد استخدم ألوان متوافقة مع Amoled؟", "@useAmoledTheme": { "type": "text", "placeholders": {} @@ -1507,14 +1507,14 @@ "unreadCount": {} } }, - "unreadMessages": "", + "unreadMessages": "{unreadEvents} رسالة غير مقروءة", "@unreadMessages": { "type": "text", "placeholders": { "unreadEvents": {} } }, - "unreadMessagesInChats": "", + "unreadMessagesInChats": "{unreadEvents} رسالة غير مقروءة من {unreadChats} محادثة", "@unreadMessagesInChats": { "type": "text", "placeholders": { @@ -1702,5 +1702,10 @@ "@yourOwnUsername": { "type": "text", "placeholders": {} + }, + "privacy": "الخصوصية", + "@privacy": { + "type": "text", + "placeholders": {} } } From 6b70cd1fe93f8c1b2b736201b28fabc0469fc079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=88=B1=E9=85=B1?= Date: Sat, 10 Oct 2020 14:14:20 +0000 Subject: [PATCH 04/20] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (313 of 313 strings) Translation: FluffyChat/Translations-New Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations-new/zh_Hans/ --- lib/l10n/intl_zh.arb | 574 +++++++++++++++++++++++++------------------ 1 file changed, 332 insertions(+), 242 deletions(-) diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index 1c851f5..a5295f6 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -92,7 +92,7 @@ "type": "text", "placeholders": {} }, - "askSSSSSign": "", + "askSSSSSign": "请输入您的安全存储的密码短语或恢复密钥,以向对方签名。", "@askSSSSSign": { "type": "text", "placeholders": {} @@ -273,7 +273,7 @@ "type": "text", "placeholders": {} }, - "changeTheme": "", + "changeTheme": "改变风格", "@changeTheme": { "type": "text", "placeholders": {} @@ -323,118 +323,118 @@ "type": "text", "placeholders": {} }, - "compareEmojiMatch": "对比并确认这些表情匹配其他那些设备", + "compareEmojiMatch": "对比并确认这些表情匹配其他那些设备:", "@compareEmojiMatch": { "type": "text", "placeholders": {} }, - "compareNumbersMatch": "", + "compareNumbersMatch": "比较以下数字,确保它们和另一设备上的相同:", "@compareNumbersMatch": { "type": "text", "placeholders": {} }, - "confirm": "", + "confirm": "确认", "@confirm": { "type": "text", "placeholders": {} }, - "connect": "", + "connect": "连接", "@connect": { "type": "text", "placeholders": {} }, - "connectionAttemptFailed": "", + "connectionAttemptFailed": "连接尝试失败", "@connectionAttemptFailed": { "type": "text", "placeholders": {} }, - "contactHasBeenInvitedToTheGroup": "", + "contactHasBeenInvitedToTheGroup": "联系人已被邀请至群组", "@contactHasBeenInvitedToTheGroup": { "type": "text", "placeholders": {} }, - "contentViewer": "", + "contentViewer": "内容查看器", "@contentViewer": { "type": "text", "placeholders": {} }, - "copiedToClipboard": "", + "copiedToClipboard": "已复制到剪贴板", "@copiedToClipboard": { "type": "text", "placeholders": {} }, - "copy": "", + "copy": "复制", "@copy": { "type": "text", "placeholders": {} }, - "couldNotDecryptMessage": "", + "couldNotDecryptMessage": "不能解密消息:{error}", "@couldNotDecryptMessage": { "type": "text", "placeholders": { "error": {} } }, - "couldNotSetAvatar": "", + "couldNotSetAvatar": "不能设定头像", "@couldNotSetAvatar": { "type": "text", "placeholders": {} }, - "couldNotSetDisplayname": "", + "couldNotSetDisplayname": "不能设定显示名称", "@couldNotSetDisplayname": { "type": "text", "placeholders": {} }, - "countParticipants": "", + "countParticipants": "{count} 参与者", "@countParticipants": { "type": "text", "placeholders": { "count": {} } }, - "create": "", + "create": "创建", "@create": { "type": "text", "placeholders": {} }, - "createAccountNow": "", + "createAccountNow": "现在创建账户", "@createAccountNow": { "type": "text", "placeholders": {} }, - "createdTheChat": "", + "createdTheChat": "{username} 创建了聊天", "@createdTheChat": { "type": "text", "placeholders": { "username": {} } }, - "createNewGroup": "", + "createNewGroup": "创建新群组", "@createNewGroup": { "type": "text", "placeholders": {} }, - "crossSigningDisabled": "", + "crossSigningDisabled": "Cross-Signing未启用", "@crossSigningDisabled": { "type": "text", "placeholders": {} }, - "crossSigningEnabled": "", + "crossSigningEnabled": "Cross-Signing已启用", "@crossSigningEnabled": { "type": "text", "placeholders": {} }, - "currentlyActive": "", + "currentlyActive": "目前活跃", "@currentlyActive": { "type": "text", "placeholders": {} }, - "darkTheme": "", + "darkTheme": "深色", "@darkTheme": { "type": "text", "placeholders": {} }, - "dateAndTimeOfDay": "", + "dateAndTimeOfDay": "{date}, {timeOfDay}", "@dateAndTimeOfDay": { "type": "text", "placeholders": { @@ -442,7 +442,7 @@ "timeOfDay": {} } }, - "dateWithoutYear": "", + "dateWithoutYear": "{month}-{day}", "@dateWithoutYear": { "type": "text", "placeholders": { @@ -450,7 +450,7 @@ "day": {} } }, - "dateWithYear": "", + "dateWithYear": "{year}-{month}-{day}", "@dateWithYear": { "type": "text", "placeholders": { @@ -459,211 +459,211 @@ "day": {} } }, - "delete": "", + "delete": "删除", "@delete": { "type": "text", "placeholders": {} }, - "deleteMessage": "", + "deleteMessage": "删除消息", "@deleteMessage": { "type": "text", "placeholders": {} }, - "deny": "", + "deny": "否认", "@deny": { "type": "text", "placeholders": {} }, - "device": "", + "device": "设备", "@device": { "type": "text", "placeholders": {} }, - "devices": "", + "devices": "设备", "@devices": { "type": "text", "placeholders": {} }, - "discardPicture": "", + "discardPicture": "丢弃图片", "@discardPicture": { "type": "text", "placeholders": {} }, - "displaynameHasBeenChanged": "", + "displaynameHasBeenChanged": "显示名称已被改变", "@displaynameHasBeenChanged": { "type": "text", "placeholders": {} }, - "donate": "", + "donate": "捐助", "@donate": { "type": "text", "placeholders": {} }, - "downloadFile": "", + "downloadFile": "下载文件", "@downloadFile": { "type": "text", "placeholders": {} }, - "editDisplayname": "", + "editDisplayname": "编辑显示名称", "@editDisplayname": { "type": "text", "placeholders": {} }, - "editJitsiInstance": "", + "editJitsiInstance": "编辑Jitsi实例", "@editJitsiInstance": { "type": "text", "placeholders": {} }, - "emoteExists": "", + "emoteExists": "表情已存在!", "@emoteExists": { "type": "text", "placeholders": {} }, - "emoteInvalid": "", + "emoteInvalid": "无效的表情快捷码!", "@emoteInvalid": { "type": "text", "placeholders": {} }, - "emoteSettings": "", + "emoteSettings": "表情设置", "@emoteSettings": { "type": "text", "placeholders": {} }, - "emoteShortcode": "", + "emoteShortcode": "表情快捷码", "@emoteShortcode": { "type": "text", "placeholders": {} }, - "emoteWarnNeedToPick": "", + "emoteWarnNeedToPick": "你需要取一个快捷码和一张图片!", "@emoteWarnNeedToPick": { "type": "text", "placeholders": {} }, - "emptyChat": "", + "emptyChat": "空聊天", "@emptyChat": { "type": "text", "placeholders": {} }, - "enableEncryptionWarning": "", + "enableEncryptionWarning": "你将不能再停用加密,确定吗?", "@enableEncryptionWarning": { "type": "text", "placeholders": {} }, - "encryption": "", + "encryption": "加密", "@encryption": { "type": "text", "placeholders": {} }, - "encryptionAlgorithm": "", + "encryptionAlgorithm": "加密算法", "@encryptionAlgorithm": { "type": "text", "placeholders": {} }, - "encryptionNotEnabled": "", + "encryptionNotEnabled": "加密未启用", "@encryptionNotEnabled": { "type": "text", "placeholders": {} }, - "end2endEncryptionSettings": "", + "end2endEncryptionSettings": "端到端加密设置", "@end2endEncryptionSettings": { "type": "text", "placeholders": {} }, - "endedTheCall": "", + "endedTheCall": "{senderName} 结束了通话", "@endedTheCall": { "type": "text", "placeholders": { "senderName": {} } }, - "enterAGroupName": "", + "enterAGroupName": "输入群组名称", "@enterAGroupName": { "type": "text", "placeholders": {} }, - "enterAUsername": "", + "enterAUsername": "输入用户名", "@enterAUsername": { "type": "text", "placeholders": {} }, - "enterYourHomeserver": "", + "enterYourHomeserver": "输入服务器地址", "@enterYourHomeserver": { "type": "text", "placeholders": {} }, - "fileName": "", + "fileName": "文件名", "@fileName": { "type": "text", "placeholders": {} }, - "fileSize": "", + "fileSize": "文件大小", "@fileSize": { "type": "text", "placeholders": {} }, - "fluffychat": "", + "fluffychat": "FluffyChat", "@fluffychat": { "type": "text", "placeholders": {} }, - "forward": "", + "forward": "转发", "@forward": { "type": "text", "placeholders": {} }, - "friday": "", + "friday": "星期五", "@friday": { "type": "text", "placeholders": {} }, - "fromJoining": "", + "fromJoining": "自加入起", "@fromJoining": { "type": "text", "placeholders": {} }, - "fromTheInvitation": "", + "fromTheInvitation": "自邀请起", "@fromTheInvitation": { "type": "text", "placeholders": {} }, - "group": "", + "group": "群组", "@group": { "type": "text", "placeholders": {} }, - "groupDescription": "", + "groupDescription": "群组描述", "@groupDescription": { "type": "text", "placeholders": {} }, - "groupDescriptionHasBeenChanged": "", + "groupDescriptionHasBeenChanged": "群组描述已被更改", "@groupDescriptionHasBeenChanged": { "type": "text", "placeholders": {} }, - "groupIsPublic": "", + "groupIsPublic": "群组是公开的", "@groupIsPublic": { "type": "text", "placeholders": {} }, - "groupWith": "", + "groupWith": "名称为{displayname}的群组", "@groupWith": { "type": "text", "placeholders": { "displayname": {} } }, - "guestsAreForbidden": "", + "guestsAreForbidden": "访客被禁止", "@guestsAreForbidden": { "type": "text", "placeholders": {} }, - "guestsCanJoin": "", + "guestsCanJoin": "访客可以加入", "@guestsCanJoin": { "type": "text", "placeholders": {} }, - "hasWithdrawnTheInvitationFor": "", + "hasWithdrawnTheInvitationFor": "{username} 撤回了对 {targetName} 的邀请", "@hasWithdrawnTheInvitationFor": { "type": "text", "placeholders": { @@ -671,49 +671,49 @@ "targetName": {} } }, - "help": "", + "help": "帮助", "@help": { "type": "text", "placeholders": {} }, - "homeserverIsNotCompatible": "", + "homeserverIsNotCompatible": "服务器不兼容", "@homeserverIsNotCompatible": { "type": "text", "placeholders": {} }, - "id": "", + "id": "ID", "@id": { "type": "text", "placeholders": {} }, - "identity": "", + "identity": "身份", "@identity": { "type": "text", "placeholders": {} }, - "incorrectPassphraseOrKey": "", + "incorrectPassphraseOrKey": "不正确的密码短语或恢复密钥", "@incorrectPassphraseOrKey": { "type": "text", "placeholders": {} }, - "inviteContact": "", + "inviteContact": "邀请联系人", "@inviteContact": { "type": "text", "placeholders": {} }, - "inviteContactToGroup": "", + "inviteContactToGroup": "邀请联系人到 {groupName}", "@inviteContactToGroup": { "type": "text", "placeholders": { "groupName": {} } }, - "invited": "", + "invited": "已邀请", "@invited": { "type": "text", "placeholders": {} }, - "invitedUser": "", + "invitedUser": "{username} 邀请了 {targetName}", "@invitedUser": { "type": "text", "placeholders": { @@ -721,12 +721,12 @@ "targetName": {} } }, - "invitedUsersOnly": "", + "invitedUsersOnly": "仅被邀请用户", "@invitedUsersOnly": { "type": "text", "placeholders": {} }, - "inviteText": "", + "inviteText": "{username} 邀请您到 FluffyChat. \n1. 安装 FluffyChat: https://fluffychat.im \n2. 注册或登录 \n3. 打开该邀请链接: {link}", "@inviteText": { "type": "text", "placeholders": { @@ -734,39 +734,39 @@ "link": {} } }, - "isDeviceKeyCorrect": "", + "isDeviceKeyCorrect": "下列设备密钥是否正确?", "@isDeviceKeyCorrect": { "type": "text", "placeholders": {} }, - "isTyping": "", + "isTyping": "正在打字...", "@isTyping": { "type": "text", "placeholders": {} }, - "joinedTheChat": "", + "joinedTheChat": "{username} 加入了聊天", "@joinedTheChat": { "type": "text", "placeholders": { "username": {} } }, - "joinRoom": "", + "joinRoom": "加入聊天室", "@joinRoom": { "type": "text", "placeholders": {} }, - "keysCached": "", + "keysCached": "密钥已被缓存", "@keysCached": { "type": "text", "placeholders": {} }, - "keysMissing": "", + "keysMissing": "密钥缺失", "@keysMissing": { "type": "text", "placeholders": {} }, - "kicked": "", + "kicked": "{username} 踢了 {targetName}", "@kicked": { "type": "text", "placeholders": { @@ -774,7 +774,7 @@ "targetName": {} } }, - "kickedAndBanned": "", + "kickedAndBanned": "{username} 踢了 {targetName} 并将其封锁", "@kickedAndBanned": { "type": "text", "placeholders": { @@ -782,385 +782,385 @@ "targetName": {} } }, - "kickFromChat": "", + "kickFromChat": "从聊天室移除", "@kickFromChat": { "type": "text", "placeholders": {} }, - "lastActiveAgo": "", + "lastActiveAgo": "上次活跃: {localizedTimeShort}", "@lastActiveAgo": { "type": "text", "placeholders": { "localizedTimeShort": {} } }, - "lastSeenIp": "", + "lastSeenIp": "上次使用的IP", "@lastSeenIp": { "type": "text", "placeholders": {} }, - "lastSeenLongTimeAgo": "", + "lastSeenLongTimeAgo": "很长时间未上线", "@lastSeenLongTimeAgo": { "type": "text", "placeholders": {} }, - "leave": "", + "leave": "离开", "@leave": { "type": "text", "placeholders": {} }, - "leftTheChat": "", + "leftTheChat": "离开了聊天", "@leftTheChat": { "type": "text", "placeholders": {} }, - "license": "", + "license": "许可证", "@license": { "type": "text", "placeholders": {} }, - "lightTheme": "", + "lightTheme": "浅色", "@lightTheme": { "type": "text", "placeholders": {} }, - "loadCountMoreParticipants": "", + "loadCountMoreParticipants": "加载 {count} 个更多的参与者", "@loadCountMoreParticipants": { "type": "text", "placeholders": { "count": {} } }, - "loadingPleaseWait": "", + "loadingPleaseWait": "加载中...请等待", "@loadingPleaseWait": { "type": "text", "placeholders": {} }, - "loadMore": "", + "loadMore": "加载更多...", "@loadMore": { "type": "text", "placeholders": {} }, - "login": "", + "login": "登入", "@login": { "type": "text", "placeholders": {} }, - "logInTo": "", + "logInTo": "登入 {homeserver}", "@logInTo": { "type": "text", "placeholders": { "homeserver": {} } }, - "logout": "", + "logout": "登出", "@logout": { "type": "text", "placeholders": {} }, - "makeAModerator": "", + "makeAModerator": "创建监管者", "@makeAModerator": { "type": "text", "placeholders": {} }, - "makeAnAdmin": "", + "makeAnAdmin": "创建管理员", "@makeAnAdmin": { "type": "text", "placeholders": {} }, - "makeSureTheIdentifierIsValid": "", + "makeSureTheIdentifierIsValid": "确保识别码正确", "@makeSureTheIdentifierIsValid": { "type": "text", "placeholders": {} }, - "messageWillBeRemovedWarning": "", + "messageWillBeRemovedWarning": "消息将对所有参与者移除", "@messageWillBeRemovedWarning": { "type": "text", "placeholders": {} }, - "moderator": "", + "moderator": "监管者", "@moderator": { "type": "text", "placeholders": {} }, - "monday": "", + "monday": "星期一", "@monday": { "type": "text", "placeholders": {} }, - "muteChat": "", + "muteChat": "将该聊天静音", "@muteChat": { "type": "text", "placeholders": {} }, - "needPantalaimonWarning": "", + "needPantalaimonWarning": "请注意当前您需要Pantalaimon以使用端到端加密功能。", "@needPantalaimonWarning": { "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "", + "newMessageInFluffyChat": "来自 FluffyChat 的新消息", "@newMessageInFluffyChat": { "type": "text", "placeholders": {} }, - "newPrivateChat": "", + "newPrivateChat": "新私密聊天", "@newPrivateChat": { "type": "text", "placeholders": {} }, - "newVerificationRequest": "", + "newVerificationRequest": "新的验证请求!", "@newVerificationRequest": { "type": "text", "placeholders": {} }, - "noCrossSignBootstrap": "", + "noCrossSignBootstrap": "Fluffychat目前不支持启用Cross-Signing. 请在Riot中启用.", "@noCrossSignBootstrap": { "type": "text", "placeholders": {} }, - "noEmotesFound": "", + "noEmotesFound": "未找到表情。😕", "@noEmotesFound": { "type": "text", "placeholders": {} }, - "noGoogleServicesWarning": "", + "noGoogleServicesWarning": "看起来您手机上没有谷歌服务框架。这对您保护隐私而言是个好决定!为收取FluffyChat的推送通知,推荐您使用microG: https://microg.org/", "@noGoogleServicesWarning": { "type": "text", "placeholders": {} }, - "noMegolmBootstrap": "", + "noMegolmBootstrap": "Fluffychat目前不支持启用在线密钥备份. 请在Riot中启用.", "@noMegolmBootstrap": { "type": "text", "placeholders": {} }, - "none": "", + "none": "无", "@none": { "type": "text", "placeholders": {} }, - "noPermission": "", + "noPermission": "没有权限", "@noPermission": { "type": "text", "placeholders": {} }, - "noRoomsFound": "", + "noRoomsFound": "未找到聊天室...", "@noRoomsFound": { "type": "text", "placeholders": {} }, - "notSupportedInWeb": "", + "notSupportedInWeb": "在网页版不支持", "@notSupportedInWeb": { "type": "text", "placeholders": {} }, - "numberSelected": "", + "numberSelected": "{number} 已选择", "@numberSelected": { "type": "text", "placeholders": { "number": {} } }, - "ok": "", + "ok": "ok", "@ok": { "type": "text", "placeholders": {} }, - "onlineKeyBackupDisabled": "", + "onlineKeyBackupDisabled": "在线密钥备份被停用", "@onlineKeyBackupDisabled": { "type": "text", "placeholders": {} }, - "onlineKeyBackupEnabled": "", + "onlineKeyBackupEnabled": "在线密钥备份已启用", "@onlineKeyBackupEnabled": { "type": "text", "placeholders": {} }, - "oopsSomethingWentWrong": "", + "oopsSomethingWentWrong": "哦!出了一些错误...", "@oopsSomethingWentWrong": { "type": "text", "placeholders": {} }, - "openAppToReadMessages": "", + "openAppToReadMessages": "打开应用以查看消息", "@openAppToReadMessages": { "type": "text", "placeholders": {} }, - "openCamera": "", + "openCamera": "打开相机", "@openCamera": { "type": "text", "placeholders": {} }, - "optionalGroupName": "", + "optionalGroupName": "(可选) 群组名称", "@optionalGroupName": { "type": "text", "placeholders": {} }, - "participatingUserDevices": "", + "participatingUserDevices": "参与者的设备", "@participatingUserDevices": { "type": "text", "placeholders": {} }, - "passphraseOrKey": "", + "passphraseOrKey": "密码短语或恢复密钥", "@passphraseOrKey": { "type": "text", "placeholders": {} }, - "password": "", + "password": "密码", "@password": { "type": "text", "placeholders": {} }, - "pickImage": "", + "pickImage": "选择图像", "@pickImage": { "type": "text", "placeholders": {} }, - "pin": "", + "pin": "固定", "@pin": { "type": "text", "placeholders": {} }, - "play": "", + "play": "播放 {fileName}", "@play": { "type": "text", "placeholders": { "fileName": {} } }, - "pleaseChooseAUsername": "", + "pleaseChooseAUsername": "请选择用户名", "@pleaseChooseAUsername": { "type": "text", "placeholders": {} }, - "pleaseEnterAMatrixIdentifier": "", + "pleaseEnterAMatrixIdentifier": "请输入matrix识别码", "@pleaseEnterAMatrixIdentifier": { "type": "text", "placeholders": {} }, - "pleaseEnterYourPassword": "", + "pleaseEnterYourPassword": "请输入您的密码", "@pleaseEnterYourPassword": { "type": "text", "placeholders": {} }, - "pleaseEnterYourUsername": "", + "pleaseEnterYourUsername": "请输入您的用户名", "@pleaseEnterYourUsername": { "type": "text", "placeholders": {} }, - "publicRooms": "", + "publicRooms": "公开聊天室", "@publicRooms": { "type": "text", "placeholders": {} }, - "recording": "", + "recording": "录制", "@recording": { "type": "text", "placeholders": {} }, - "redactedAnEvent": "", + "redactedAnEvent": "{username} 编辑了一个事件", "@redactedAnEvent": { "type": "text", "placeholders": { "username": {} } }, - "reject": "", + "reject": "拒绝", "@reject": { "type": "text", "placeholders": {} }, - "rejectedTheInvitation": "", + "rejectedTheInvitation": "{username} 拒绝了邀请", "@rejectedTheInvitation": { "type": "text", "placeholders": { "username": {} } }, - "rejoin": "", + "rejoin": "重新加入", "@rejoin": { "type": "text", "placeholders": {} }, - "remove": "", + "remove": "移除", "@remove": { "type": "text", "placeholders": {} }, - "removeAllOtherDevices": "", + "removeAllOtherDevices": "移除其他全部设备", "@removeAllOtherDevices": { "type": "text", "placeholders": {} }, - "removedBy": "", + "removedBy": "被{username}移除", "@removedBy": { "type": "text", "placeholders": { "username": {} } }, - "removeDevice": "", + "removeDevice": "移除设备", "@removeDevice": { "type": "text", "placeholders": {} }, - "removeExile": "", + "removeExile": "移除流放", "@removeExile": { "type": "text", "placeholders": {} }, - "removeMessage": "", + "removeMessage": "移除消息", "@removeMessage": { "type": "text", "placeholders": {} }, - "renderRichContent": "", + "renderRichContent": "渲染富文本内容", "@renderRichContent": { "type": "text", "placeholders": {} }, - "reply": "", + "reply": "回复", "@reply": { "type": "text", "placeholders": {} }, - "requestPermission": "", + "requestPermission": "请求权限", "@requestPermission": { "type": "text", "placeholders": {} }, - "requestToReadOlderMessages": "", + "requestToReadOlderMessages": "请求读取旧的消息", "@requestToReadOlderMessages": { "type": "text", "placeholders": {} }, - "revokeAllPermissions": "", + "revokeAllPermissions": "撤销全部权限", "@revokeAllPermissions": { "type": "text", "placeholders": {} }, - "roomHasBeenUpgraded": "", + "roomHasBeenUpgraded": "聊天室已升级", "@roomHasBeenUpgraded": { "type": "text", "placeholders": {} }, - "saturday": "", + "saturday": "星期六", "@saturday": { "type": "text", "placeholders": {} }, - "searchForAChat": "", + "searchForAChat": "搜索聊天室", "@searchForAChat": { "type": "text", "placeholders": {} }, - "seenByUser": "", + "seenByUser": "被 {username} 看见", "@seenByUser": { "type": "text", "placeholders": { "username": {} } }, - "seenByUserAndCountOthers": "", + "seenByUserAndCountOthers": "被 {username} 和 {count} 个其他人看见", "@seenByUserAndCountOthers": { "type": "text", "placeholders": { @@ -1168,7 +1168,7 @@ "count": {} } }, - "seenByUserAndUser": "", + "seenByUserAndUser": "被 {username} 和 {username2} 看见", "@seenByUserAndUser": { "type": "text", "placeholders": { @@ -1176,183 +1176,183 @@ "username2": {} } }, - "send": "", + "send": "发送", "@send": { "type": "text", "placeholders": {} }, - "sendAMessage": "", + "sendAMessage": "发送一条消息", "@sendAMessage": { "type": "text", "placeholders": {} }, - "sendFile": "", + "sendFile": "发送文件", "@sendFile": { "type": "text", "placeholders": {} }, - "sendImage": "", + "sendImage": "发送图像", "@sendImage": { "type": "text", "placeholders": {} }, - "sentAFile": "", + "sentAFile": "{username} 发送了文件", "@sentAFile": { "type": "text", "placeholders": { "username": {} } }, - "sentAnAudio": "", + "sentAnAudio": "{username} 发送了音频", "@sentAnAudio": { "type": "text", "placeholders": { "username": {} } }, - "sentAPicture": "", + "sentAPicture": "{username} 发送了图片", "@sentAPicture": { "type": "text", "placeholders": { "username": {} } }, - "sentASticker": "", + "sentASticker": "{username} 发送了贴纸", "@sentASticker": { "type": "text", "placeholders": { "username": {} } }, - "sentAVideo": "", + "sentAVideo": "{username} 发送了视频", "@sentAVideo": { "type": "text", "placeholders": { "username": {} } }, - "sentCallInformations": "", + "sentCallInformations": "{senderName} 发送了通话信息", "@sentCallInformations": { "type": "text", "placeholders": { "senderName": {} } }, - "sessionVerified": "", + "sessionVerified": "会话已验证", "@sessionVerified": { "type": "text", "placeholders": {} }, - "setAProfilePicture": "", + "setAProfilePicture": "设置个人资料图片", "@setAProfilePicture": { "type": "text", "placeholders": {} }, - "setGroupDescription": "", + "setGroupDescription": "设置群组描述", "@setGroupDescription": { "type": "text", "placeholders": {} }, - "setInvitationLink": "", + "setInvitationLink": "设置邀请链接", "@setInvitationLink": { "type": "text", "placeholders": {} }, - "setStatus": "", + "setStatus": "设置状态", "@setStatus": { "type": "text", "placeholders": {} }, - "settings": "", + "settings": "设置", "@settings": { "type": "text", "placeholders": {} }, - "share": "", + "share": "分享", "@share": { "type": "text", "placeholders": {} }, - "sharedTheLocation": "", + "sharedTheLocation": "{username} 分享了位置", "@sharedTheLocation": { "type": "text", "placeholders": { "username": {} } }, - "signUp": "", + "signUp": "注册", "@signUp": { "type": "text", "placeholders": {} }, - "skip": "", + "skip": "跳过", "@skip": { "type": "text", "placeholders": {} }, - "sourceCode": "", + "sourceCode": "源代码", "@sourceCode": { "type": "text", "placeholders": {} }, - "startedACall": "", + "startedACall": "{senderName} 开始了通话", "@startedACall": { "type": "text", "placeholders": { "senderName": {} } }, - "startYourFirstChat": "", + "startYourFirstChat": "开始你的第一个聊天 :-)", "@startYourFirstChat": { "type": "text", "placeholders": {} }, - "statusExampleMessage": "", + "statusExampleMessage": "你今天怎么样?", "@statusExampleMessage": { "type": "text", "placeholders": {} }, - "submit": "", + "submit": "提交", "@submit": { "type": "text", "placeholders": {} }, - "sunday": "", + "sunday": "星期日", "@sunday": { "type": "text", "placeholders": {} }, - "systemTheme": "", + "systemTheme": "系统", "@systemTheme": { "type": "text", "placeholders": {} }, - "tapToShowMenu": "", + "tapToShowMenu": "点击以显示菜单", "@tapToShowMenu": { "type": "text", "placeholders": {} }, - "theyDontMatch": "", + "theyDontMatch": "它们不匹配", "@theyDontMatch": { "type": "text", "placeholders": {} }, - "theyMatch": "", + "theyMatch": "它们匹配", "@theyMatch": { "type": "text", "placeholders": {} }, - "thisRoomHasBeenArchived": "", + "thisRoomHasBeenArchived": "该聊天室已被归档。", "@thisRoomHasBeenArchived": { "type": "text", "placeholders": {} }, - "thursday": "", + "thursday": "星期四", "@thursday": { "type": "text", "placeholders": {} }, - "timeOfDay": "", + "timeOfDay": "{hours12}:{minutes} {suffix}", "@timeOfDay": { "type": "text", "placeholders": { @@ -1362,23 +1362,23 @@ "suffix": {} } }, - "title": "", + "title": "FluffyChat", "@title": { "description": "Title for the application", "type": "text", "placeholders": {} }, - "tryToSendAgain": "", + "tryToSendAgain": "尝试重新发送", "@tryToSendAgain": { "type": "text", "placeholders": {} }, - "tuesday": "", + "tuesday": "星期二", "@tuesday": { "type": "text", "placeholders": {} }, - "unbannedUser": "", + "unbannedUser": "{username} 解除了 {targetName} 的封锁", "@unbannedUser": { "type": "text", "placeholders": { @@ -1386,58 +1386,58 @@ "targetName": {} } }, - "unblockDevice": "", + "unblockDevice": "解锁设备", "@unblockDevice": { "type": "text", "placeholders": {} }, - "unknownDevice": "", + "unknownDevice": "未知设备", "@unknownDevice": { "type": "text", "placeholders": {} }, - "unknownEncryptionAlgorithm": "", + "unknownEncryptionAlgorithm": "未知加密算法", "@unknownEncryptionAlgorithm": { "type": "text", "placeholders": {} }, - "unknownEvent": "", + "unknownEvent": "未知事件 '{type}'", "@unknownEvent": { "type": "text", "placeholders": { "type": {} } }, - "unknownSessionVerify": "", + "unknownSessionVerify": "未知会话,请验证", "@unknownSessionVerify": { "type": "text", "placeholders": {} }, - "unmuteChat": "", + "unmuteChat": "解除聊天的静音", "@unmuteChat": { "type": "text", "placeholders": {} }, - "unpin": "", + "unpin": "取消固定", "@unpin": { "type": "text", "placeholders": {} }, - "unreadChats": "", + "unreadChats": "{unreadCount} 未读聊天", "@unreadChats": { "type": "text", "placeholders": { "unreadCount": {} } }, - "unreadMessages": "", + "unreadMessages": "{unreadEvents} 未读消息", "@unreadMessages": { "type": "text", "placeholders": { "unreadEvents": {} } }, - "unreadMessagesInChats": "", + "unreadMessagesInChats": "来自 {unreadChats} 聊天的 {unreadEvents} 未读消息", "@unreadMessagesInChats": { "type": "text", "placeholders": { @@ -1445,12 +1445,12 @@ "unreadChats": {} } }, - "useAmoledTheme": "", + "useAmoledTheme": "使用适合Amoled屏的颜色?", "@useAmoledTheme": { "type": "text", "placeholders": {} }, - "userAndOthersAreTyping": "", + "userAndOthersAreTyping": "{username} 和 {count} 其他人正在打字...", "@userAndOthersAreTyping": { "type": "text", "placeholders": { @@ -1458,7 +1458,7 @@ "count": {} } }, - "userAndUserAreTyping": "", + "userAndUserAreTyping": "{username} 和 {username2} 正在打字...", "@userAndUserAreTyping": { "type": "text", "placeholders": { @@ -1466,26 +1466,26 @@ "username2": {} } }, - "userIsTyping": "", + "userIsTyping": "{username} 正在打字...", "@userIsTyping": { "type": "text", "placeholders": { "username": {} } }, - "userLeftTheChat": "", + "userLeftTheChat": "{username} 离开了聊天", "@userLeftTheChat": { "type": "text", "placeholders": { "username": {} } }, - "username": "", + "username": "用户名", "@username": { "type": "text", "placeholders": {} }, - "userSentUnknownEvent": "", + "userSentUnknownEvent": "{username} 发送了一个 {type} 事件", "@userSentUnknownEvent": { "type": "text", "placeholders": { @@ -1493,144 +1493,234 @@ "type": {} } }, - "verifiedSession": "", + "verifiedSession": "成功验证会话!", "@verifiedSession": { "type": "text", "placeholders": {} }, - "verify": "", + "verify": "验证", "@verify": { "type": "text", "placeholders": {} }, - "verifyManual": "", + "verifyManual": "手动验证", "@verifyManual": { "type": "text", "placeholders": {} }, - "verifyStart": "", + "verifyStart": "开始验证", "@verifyStart": { "type": "text", "placeholders": {} }, - "verifySuccess": "", + "verifySuccess": "您已成功验证!", "@verifySuccess": { "type": "text", "placeholders": {} }, - "verifyTitle": "", + "verifyTitle": "验证其他账号", "@verifyTitle": { "type": "text", "placeholders": {} }, - "verifyUser": "", + "verifyUser": "验证用户", "@verifyUser": { "type": "text", "placeholders": {} }, - "videoCall": "", + "videoCall": "视频通话", "@videoCall": { "type": "text", "placeholders": {} }, - "visibilityOfTheChatHistory": "", + "visibilityOfTheChatHistory": "聊天记录的可见性", "@visibilityOfTheChatHistory": { "type": "text", "placeholders": {} }, - "visibleForAllParticipants": "", + "visibleForAllParticipants": "对所有参与者可见", "@visibleForAllParticipants": { "type": "text", "placeholders": {} }, - "visibleForEveryone": "", + "visibleForEveryone": "对所有人可见", "@visibleForEveryone": { "type": "text", "placeholders": {} }, - "voiceMessage": "", + "voiceMessage": "语音消息", "@voiceMessage": { "type": "text", "placeholders": {} }, - "waitingPartnerAcceptRequest": "", + "waitingPartnerAcceptRequest": "等待对方接受请求...", "@waitingPartnerAcceptRequest": { "type": "text", "placeholders": {} }, - "waitingPartnerEmoji": "", + "waitingPartnerEmoji": "等待对方接受emoji...", "@waitingPartnerEmoji": { "type": "text", "placeholders": {} }, - "waitingPartnerNumbers": "", + "waitingPartnerNumbers": "等待对方接受数字...", "@waitingPartnerNumbers": { "type": "text", "placeholders": {} }, - "wallpaper": "", + "wallpaper": "壁纸", "@wallpaper": { "type": "text", "placeholders": {} }, - "warningEncryptionInBeta": "", + "warningEncryptionInBeta": "端到端加密目前在测试阶段!请自行承担风险!", "@warningEncryptionInBeta": { "type": "text", "placeholders": {} }, - "wednesday": "", + "wednesday": "星期三", "@wednesday": { "type": "text", "placeholders": {} }, - "welcomeText": "", + "welcomeText": "欢迎来到matrix网络中最可爱的即时通讯应用。", "@welcomeText": { "type": "text", "placeholders": {} }, - "whoIsAllowedToJoinThisGroup": "", + "whoIsAllowedToJoinThisGroup": "谁被允许加入本群组", "@whoIsAllowedToJoinThisGroup": { "type": "text", "placeholders": {} }, - "writeAMessage": "", + "writeAMessage": "写一条消息...", "@writeAMessage": { "type": "text", "placeholders": {} }, - "yes": "", + "yes": "是", "@yes": { "type": "text", "placeholders": {} }, - "you": "", + "you": "你", "@you": { "type": "text", "placeholders": {} }, - "youAreInvitedToThisChat": "", + "youAreInvitedToThisChat": "你被邀请到该聊天", "@youAreInvitedToThisChat": { "type": "text", "placeholders": {} }, - "youAreNoLongerParticipatingInThisChat": "", + "youAreNoLongerParticipatingInThisChat": "你已不再参与此聊天", "@youAreNoLongerParticipatingInThisChat": { "type": "text", "placeholders": {} }, - "youCannotInviteYourself": "", + "youCannotInviteYourself": "你不能邀请自己", "@youCannotInviteYourself": { "type": "text", "placeholders": {} }, - "youHaveBeenBannedFromThisChat": "", + "youHaveBeenBannedFromThisChat": "你已被该聊天封锁", "@youHaveBeenBannedFromThisChat": { "type": "text", "placeholders": {} }, - "yourOwnUsername": "", + "yourOwnUsername": "你自己的用户名", "@yourOwnUsername": { "type": "text", "placeholders": {} + }, + "warning": "警告!", + "@warning": { + "type": "text", + "placeholders": {} + }, + "sendVideo": "发送视频", + "@sendVideo": { + "type": "text", + "placeholders": {} + }, + "sendOriginal": "发送原创内容", + "@sendOriginal": { + "type": "text", + "placeholders": {} + }, + "sendAudio": "发送音频", + "@sendAudio": { + "type": "text", + "placeholders": {} + }, + "no": "不", + "@no": { + "type": "text", + "placeholders": {} + }, + "changesHaveBeenSaved": "更改已被保存", + "@changesHaveBeenSaved": { + "type": "text", + "placeholders": {} + }, + "sentryInfo": "关于您隐私的信息: https://sentry.io/security/", + "@sentryInfo": { + "type": "text", + "placeholders": {} + }, + "sendBugReports": "允许向sentry.io发送错误报告", + "@sendBugReports": { + "type": "text", + "placeholders": {} + }, + "privacy": "隐私", + "@privacy": { + "type": "text", + "placeholders": {} + }, + "passwordHasBeenChanged": "密码已被更改", + "@passwordHasBeenChanged": { + "type": "text", + "placeholders": {} + }, + "ignoreListDescription": "你可以忽略打扰你的用户。你将不会收到来自忽略列表中用户的任何消息或聊天室邀请。", + "@ignoreListDescription": { + "type": "text", + "placeholders": {} + }, + "ignoreUsername": "忽略用户名", + "@ignoreUsername": { + "type": "text", + "placeholders": {} + }, + "ignoredUsers": "已忽略的用户", + "@ignoredUsers": { + "type": "text", + "placeholders": {} + }, + "enableEmotesGlobally": "在全局启用表情包", + "@enableEmotesGlobally": { + "type": "text", + "placeholders": {} + }, + "emotePacks": "聊天室的表情包", + "@emotePacks": { + "type": "text", + "placeholders": {} + }, + "deleteAccount": "删除账号", + "@deleteAccount": { + "type": "text", + "placeholders": {} + }, + "deactivateAccountWarning": "这将停用您的用户账号。这不能被撤销,您确定吗?", + "@deactivateAccountWarning": { + "type": "text", + "placeholders": {} + }, + "changeDeviceName": "更改设备名称", + "@changeDeviceName": { + "type": "text", + "placeholders": {} } } From 6a6845406a03c05b9e7c52776da521d889403d23 Mon Sep 17 00:00:00 2001 From: lucanomax Date: Sat, 10 Oct 2020 21:08:20 +0000 Subject: [PATCH 05/20] Translated using Weblate (Italian) Currently translated at 25.2% (79 of 313 strings) Translation: FluffyChat/Translations-New Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations-new/it/ --- lib/l10n/intl_it.arb | 267 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index bf5cc93..c5cbc6f 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -194,5 +194,272 @@ "@about": { "type": "text", "placeholders": {} + }, + "deleteMessage": "Cancella messaggio", + "@deleteMessage": { + "type": "text", + "placeholders": {} + }, + "deleteAccount": "Elimina account", + "@deleteAccount": { + "type": "text", + "placeholders": {} + }, + "deactivateAccountWarning": "Disabiliterà il tuo account. Non puoi tornare indietro! Sei sicuro?", + "@deactivateAccountWarning": { + "type": "text", + "placeholders": {} + }, + "delete": "Cancella", + "@delete": { + "type": "text", + "placeholders": {} + }, + "dateWithYear": "{day}-{month}-{year}", + "@dateWithYear": { + "type": "text", + "placeholders": { + "year": {}, + "month": {}, + "day": {} + } + }, + "dateWithoutYear": "{month}-{day}", + "@dateWithoutYear": { + "type": "text", + "placeholders": { + "month": {}, + "day": {} + } + }, + "dateAndTimeOfDay": "{date}, {timeOfDay}", + "@dateAndTimeOfDay": { + "type": "text", + "placeholders": { + "date": {}, + "timeOfDay": {} + } + }, + "currentlyActive": "Attualmente attivo", + "@currentlyActive": { + "type": "text", + "placeholders": {} + }, + "createNewGroup": "Crea un nuovo gruppo", + "@createNewGroup": { + "type": "text", + "placeholders": {} + }, + "createdTheChat": "{username} ha creato la chat", + "@createdTheChat": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "createAccountNow": "Crea ora un account", + "@createAccountNow": { + "type": "text", + "placeholders": {} + }, + "create": "Crea", + "@create": { + "type": "text", + "placeholders": {} + }, + "countParticipants": "{count} partecipanti", + "@countParticipants": { + "type": "text", + "placeholders": { + "count": {} + } + }, + "couldNotSetDisplayname": "Impossibile impostare nome", + "@couldNotSetDisplayname": { + "type": "text", + "placeholders": {} + }, + "couldNotSetAvatar": "Impossibile impostare avatar", + "@couldNotSetAvatar": { + "type": "text", + "placeholders": {} + }, + "couldNotDecryptMessage": "Impossibile decriptare messaggio: {error}", + "@couldNotDecryptMessage": { + "type": "text", + "placeholders": { + "error": {} + } + }, + "copy": "Copia", + "@copy": { + "type": "text", + "placeholders": {} + }, + "copiedToClipboard": "Copiato negli Appunti", + "@copiedToClipboard": { + "type": "text", + "placeholders": {} + }, + "contentViewer": "Visualizzatore contenuti", + "@contentViewer": { + "type": "text", + "placeholders": {} + }, + "contactHasBeenInvitedToTheGroup": "Il contatto è stato invitato nel gruppo", + "@contactHasBeenInvitedToTheGroup": { + "type": "text", + "placeholders": {} + }, + "connectionAttemptFailed": "Tentativo di connessione fallito", + "@connectionAttemptFailed": { + "type": "text", + "placeholders": {} + }, + "connect": "Connetti", + "@connect": { + "type": "text", + "placeholders": {} + }, + "confirm": "Conferma", + "@confirm": { + "type": "text", + "placeholders": {} + }, + "compareNumbersMatch": "Confronta e assicurati che le seguenti emoji corrispondano a quelle dell'altro dispositivo:", + "@compareNumbersMatch": { + "type": "text", + "placeholders": {} + }, + "compareEmojiMatch": "Confronta e assicurati che le seguenti emoji corrispondano a quelle dell'altro dispositivo:", + "@compareEmojiMatch": { + "type": "text", + "placeholders": {} + }, + "close": "Chiudi", + "@close": { + "type": "text", + "placeholders": {} + }, + "chooseAUsername": "Scegli un username", + "@chooseAUsername": { + "type": "text", + "placeholders": {} + }, + "chooseAStrongPassword": "Scegli una password complessa", + "@chooseAStrongPassword": { + "type": "text", + "placeholders": {} + }, + "chatDetails": "Dettagli chat", + "@chatDetails": { + "type": "text", + "placeholders": {} + }, + "chat": "Chat", + "@chat": { + "type": "text", + "placeholders": {} + }, + "channelCorruptedDecryptError": "La crittografia è corrotta", + "@channelCorruptedDecryptError": { + "type": "text", + "placeholders": {} + }, + "changeTheServer": "Cambia server", + "@changeTheServer": { + "type": "text", + "placeholders": {} + }, + "changeWallpaper": "Cambia sfondo", + "@changeWallpaper": { + "type": "text", + "placeholders": {} + }, + "changeTheNameOfTheGroup": "Cambia il nome del gruppo", + "@changeTheNameOfTheGroup": { + "type": "text", + "placeholders": {} + }, + "changelog": "Registro cambiamenti", + "@changelog": { + "type": "text", + "placeholders": {} + }, + "changedTheRoomInvitationLink": "{username} ha cambiato il link di invito", + "@changedTheRoomInvitationLink": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "changedTheRoomAliases": "{username} ha cambiato il nome delle stanze", + "@changedTheRoomAliases": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "changedTheProfileAvatar": "{username} ha cambiato il loro avatar", + "@changedTheProfileAvatar": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "changedTheJoinRulesTo": "{username} ha cambiato le regole per unirsi in: {joinRules}", + "@changedTheJoinRulesTo": { + "type": "text", + "placeholders": { + "username": {}, + "joinRules": {} + } + }, + "changedTheJoinRules": "{username} ha cambiato le regole per unirsi", + "@changedTheJoinRules": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "changedTheHistoryVisibilityTo": "{username} ha cambiato la visibilità della cronologia in: {rules}", + "@changedTheHistoryVisibilityTo": { + "type": "text", + "placeholders": { + "username": {}, + "rules": {} + } + }, + "changedTheHistoryVisibility": "{username} ha cambiato la visibilità della cronologia", + "@changedTheHistoryVisibility": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "changedTheGuestAccessRulesTo": "{username} ha cambiato le regole di accesso per ospiti con: {rules}", + "@changedTheGuestAccessRulesTo": { + "type": "text", + "placeholders": { + "username": {}, + "rules": {} + } + }, + "changedTheChatAvatar": "{username} ha cambiato avatar", + "@changedTheChatAvatar": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "askSSSSSign": "Per entrare con l'altro utente, per favore inserisci la tua passphrase o recovery key.", + "@askSSSSSign": { + "type": "text", + "placeholders": {} + }, + "askSSSSCache": "Per favore inserisci la tua passphrase o recovery key per la cache delle chiavi.", + "@askSSSSCache": { + "type": "text", + "placeholders": {} } } From 4f272efbb5426397fd7edb5b8f0da32cefc6247b Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 12 Oct 2020 06:44:41 +0000 Subject: [PATCH 06/20] fix(l10n): Make en the default fallback language. --- l10n.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/l10n.yaml b/l10n.yaml index 19636a3..82662ea 100644 --- a/l10n.yaml +++ b/l10n.yaml @@ -1,4 +1,5 @@ arb-dir: lib/l10n template-arb-file: intl_en.arb output-localization-file: l10n.dart -output-class: L10n \ No newline at end of file +output-class: L10n +preferred-supported-locales: ["en"] \ No newline at end of file From f18779221f284d3d548290cc2c35694acce1cecd Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 12 Oct 2020 08:09:47 +0000 Subject: [PATCH 07/20] ci: Optimize dependencies --- .gitlab-ci.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3cea1e6..3c08604 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -158,8 +158,7 @@ upload_to_fdroid_repo: - export UPDATE_VERSION=$(pcregrep -o1 'version:\\s([0-9]*\\.[0-9]*\\.[0-9]*)\\+[0-9]*' pubspec.yaml) && mv app-release.apk "${UPDATE_VERSION}.apk" - rsync -rav -e ssh ./ fluffy@fdroid.nordgedanken.dev:/fdroid/repo - ssh fluffy@fdroid.nordgedanken.dev "cd fdroid && mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc && fdroid update" - dependencies: - - build_android_apk + needs: ["build_android_apk"] only: - tags @@ -174,8 +173,7 @@ pages: - cd build/web/ && bundle install && cd ../../ - cd build/web/ && bundle exec jekyll build -d public && cd ../../ - mv build/web/public ./ - dependencies: - - build_web + needs: ["build_web"] artifacts: paths: - public @@ -215,3 +213,4 @@ snap:publish: - './*.snap' when: on_success expire_in: 1 week + needs: [] From 1b1dfc7c81dbe72cb8b21ab32fdc4a6864f13ba9 Mon Sep 17 00:00:00 2001 From: Lukas Lihotzki Date: Mon, 12 Oct 2020 11:56:20 +0200 Subject: [PATCH 08/20] Update native_imaging --- lib/utils/resize_image.dart | 8 ++++---- pubspec.lock | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/utils/resize_image.dart b/lib/utils/resize_image.dart index 93b328f..102c78b 100644 --- a/lib/utils/resize_image.dart +++ b/lib/utils/resize_image.dart @@ -18,8 +18,8 @@ Future resizeImage(MatrixImageFile file, try { final nativeImg = native.Image(); await nativeImg.loadEncoded(file.bytes); - file.width = nativeImg.width(); - file.height = nativeImg.height(); + file.width = nativeImg.width; + file.height = nativeImg.height; args = _IsolateArgs( width: file.width, height: file.height, bytes: file.bytes, max: max); nativeImg.free(); @@ -96,8 +96,8 @@ Future<_IsolateResponse> _isolateFunction(_IsolateArgs args) async { final ret = _IsolateResponse( blurhash: blurhash, jpegBytes: jpegBytes, - width: nativeImg.width(), - height: nativeImg.height()); + width: nativeImg.width, + height: nativeImg.height); nativeImg.free(); diff --git a/pubspec.lock b/pubspec.lock index e9511b5..fea4cbd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -553,7 +553,7 @@ packages: description: path: "." ref: master - resolved-ref: bd24832f96537447174aa34ba78eaed7ff05bb8e + resolved-ref: "7fef2565e4ab0c3f6a0e0ac19a77c30ea6778e16" url: "https://gitlab.com/famedly/libraries/native_imaging.git" source: git version: "0.0.1" From 1236062768663081a81a6725ba47e7151f447ed6 Mon Sep 17 00:00:00 2001 From: Christopher James Halse Rogers Date: Tue, 13 Oct 2020 10:24:47 +1100 Subject: [PATCH 09/20] Initial snapcraft metadata --- snap/snapcraft.yaml | 59 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 snap/snapcraft.yaml diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml new file mode 100644 index 0000000..9e20cee --- /dev/null +++ b/snap/snapcraft.yaml @@ -0,0 +1,59 @@ +name: fluffychat +base: core18 # the base snap is the execution environment for this snap +version: git # just for humans, typically '1.2+git' or '1.3.2' +summary: Open. Nonprofit. Cute ♥ +description: | + FluffyChat - Chat with your friends + + 9 greatest FluffyChat features: + 1. Opensource and open development where everyone can join. + 2. Nonprofit - FluffyChat is donation funded. + 3. Cute design and many theme settings including a dark mode. + 4. Unlimited groups and direct chats. + 5. FluffyChat is made as simple to use as possible. + 6. Free to use for everyone without ads. + 7. FluffyChat can use your addressbook to find your friends or you can use + usernames. + 8. There is no "FluffyChat server" you are forced to use. Use the server + you find trustworthy or host your own. + 9. Compatible with Riot, Fractal, Nekho and all matrix messengers. + + Join the community: fluffychat://+ubports_community:matrix.org + Website: http://fluffy.chat + Microblog: https://metalhead.club/@krille + +grade: devel # must be 'stable' to release into candidate/stable channels +confinement: strict # use 'strict' once you have the right plugs and slots + +parts: + olm: + plugin: cmake + source: https://gitlab.matrix.org/matrix-org/olm.git + source-type: git + source-tag: 3.2.1 + fluffychat: + plugin: flutter + source: . + flutter-target: lib/main.dart + stage-packages: + - libsqlite3-0 + override-prime: | + snapcraftctl prime + ln -sf libsqlite3.so.0 ${SNAPCRAFT_PRIME}/usr/lib/x86_64-linux-gnu/libsqlite3.so + +slots: + dbus-svc: + interface: dbus + bus: session + name: chat.fluffy.fluffychat + +apps: + fluffychat: + command: fluffychat + extensions: + - flutter-dev + plugs: + - network + - home + slots: + - dbus-svc From 81e32c5ee421474a504dab6bc7f29be0fc3921b6 Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Tue, 13 Oct 2020 12:20:13 +0200 Subject: [PATCH 10/20] fix: LocalStorage location on desktop --- .gitignore | 1 + lib/components/matrix.dart | 4 +--- lib/main.dart | 5 ++--- lib/utils/famedlysdk_store.dart | 10 ++++++++++ lib/utils/sentry_controller.dart | 8 ++++---- pubspec.lock | 2 +- 6 files changed, 19 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index e059815..4511167 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ .svn/ lib/generated_plugin_registrant.dart google-services.json +prime # libolm package /assets/js/package/* diff --git a/lib/components/matrix.dart b/lib/components/matrix.dart index 8c0cf95..17b1fe4 100644 --- a/lib/components/matrix.dart +++ b/lib/components/matrix.dart @@ -11,7 +11,6 @@ import 'package:fluffychat/utils/user_status.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:localstorage/localstorage.dart'; import 'package:universal_html/prefer_universal/html.dart' as html; import 'package:url_launcher/url_launcher.dart'; @@ -78,8 +77,7 @@ class MatrixState extends State { void clean() async { if (!kIsWeb) return; - final storage = LocalStorage('LocalStorage'); - await storage.ready; + final storage = await getLocalStorage(); await storage.deleteItem(widget.clientName); } diff --git a/lib/main.dart b/lib/main.dart index 334eb96..a08f001 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,12 +8,12 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:localstorage/localstorage.dart'; import 'package:sentry/sentry.dart'; import 'package:universal_html/prefer_universal/html.dart' as html; import 'components/matrix.dart'; import 'components/theme_switcher.dart'; +import 'utils/famedlysdk_store.dart'; import 'views/chat_list.dart'; final sentry = SentryClient(dsn: '8591d0d863b646feb4f3dda7e5dcab38'); @@ -21,8 +21,7 @@ final sentry = SentryClient(dsn: '8591d0d863b646feb4f3dda7e5dcab38'); void captureException(error, stackTrace) async { debugPrint(error.toString()); debugPrint(stackTrace.toString()); - final storage = LocalStorage('LocalStorage'); - await storage.ready; + final storage = await getLocalStorage(); if (storage.getItem('sentry') == true) { await sentry.captureException( exception: error, diff --git a/lib/utils/famedlysdk_store.dart b/lib/utils/famedlysdk_store.dart index 52b18fc..d305ade 100644 --- a/lib/utils/famedlysdk_store.dart +++ b/lib/utils/famedlysdk_store.dart @@ -6,12 +6,22 @@ import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:localstorage/localstorage.dart'; +import 'package:path_provider/path_provider.dart'; import 'dart:async'; import 'dart:core'; import './database/shared.dart'; import 'package:olm/olm.dart' as olm; // needed for migration import 'package:random_string/random_string.dart'; +Future getLocalStorage() async { + final directory = PlatformInfos.isBetaDesktop + ? await getApplicationSupportDirectory() + : await getApplicationDocumentsDirectory(); + final localStorage = LocalStorage('LocalStorage', directory.path); + await localStorage.ready; + return localStorage; +} + Future getDatabase(Client client) async { while (_generateDatabaseLock) { await Future.delayed(Duration(milliseconds: 50)); diff --git a/lib/utils/sentry_controller.dart b/lib/utils/sentry_controller.dart index 1557262..970a419 100644 --- a/lib/utils/sentry_controller.dart +++ b/lib/utils/sentry_controller.dart @@ -2,10 +2,10 @@ import 'package:bot_toast/bot_toast.dart'; import 'package:fluffychat/components/dialogs/simple_dialogs.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:localstorage/localstorage.dart'; + +import 'famedlysdk_store.dart'; abstract class SentryController { - static LocalStorage storage = LocalStorage('LocalStorage'); static Future toggleSentryAction(BuildContext context) async { final enableSentry = await SimpleDialogs(context).askConfirmation( titleText: L10n.of(context).sendBugReports, @@ -13,14 +13,14 @@ abstract class SentryController { confirmText: L10n.of(context).ok, cancelText: L10n.of(context).no, ); - await storage.ready; + final storage = await getLocalStorage(); await storage.setItem('sentry', enableSentry); BotToast.showText(text: L10n.of(context).changesHaveBeenSaved); return; } static Future getSentryStatus() async { - await storage.ready; + final storage = await getLocalStorage(); return storage.getItem('sentry') as bool; } } diff --git a/pubspec.lock b/pubspec.lock index fea4cbd..eda8463 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1074,5 +1074,5 @@ packages: source: hosted version: "0.1.2" sdks: - dart: ">=2.10.0-110 <=2.11.0-161.0.dev" + dart: ">=2.10.0-110 <2.11.0" flutter: ">=1.20.0 <2.0.0" From 49cf5846262992434a730115bdbccde35622770c Mon Sep 17 00:00:00 2001 From: Lukas Lihotzki Date: Tue, 13 Oct 2020 13:10:36 +0200 Subject: [PATCH 11/20] Update native_imaging --- pubspec.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.lock b/pubspec.lock index eda8463..66ae763 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -553,7 +553,7 @@ packages: description: path: "." ref: master - resolved-ref: "7fef2565e4ab0c3f6a0e0ac19a77c30ea6778e16" + resolved-ref: c8eb59c25c4e3a568bd64e4722108ec45259e157 url: "https://gitlab.com/famedly/libraries/native_imaging.git" source: git version: "0.0.1" From b6c35061262045d362ece05f7f8b7c9e61364247 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Sun, 4 Oct 2020 23:22:41 +0000 Subject: [PATCH 12/20] feat: Swipe to Reply --- lib/components/swipeable.dart | 459 ++++++++++++++++++++++++++++++++++ lib/views/chat.dart | 58 +++-- 2 files changed, 502 insertions(+), 15 deletions(-) create mode 100644 lib/components/swipeable.dart diff --git a/lib/components/swipeable.dart b/lib/components/swipeable.dart new file mode 100644 index 0000000..71543dc --- /dev/null +++ b/lib/components/swipeable.dart @@ -0,0 +1,459 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +const double _kMinFlingVelocity = 700.0; +const double _kMinFlingVelocityDelta = 400.0; +const double _kFlingVelocityScale = 1.0 / 300.0; +const double _kDismissThreshold = 0.4; + +/// Signature used by [Swipeable] to indicate that it has been swiped in +/// the given `direction`. +/// +/// Used by [Swipeable.onSwiped]. +typedef SwipeDirectionCallback = void Function(SwipeDirection direction); + +/// Signature used by [Swipeable] to give the application an opportunity to +/// confirm or veto a swipe gesture. +/// +/// Used by [Swipeable.confirmSwipe]. +typedef ConfirmSwipeCallback = Future Function(SwipeDirection direction); + +/// The direction in which a [Swipeable] can be swiped. +enum SwipeDirection { + /// The [Swipeable] can be swiped by dragging either left or right. + horizontal, + + /// The [Swipeable] can be swiped by dragging in the reverse of the + /// reading direction (e.g., from right to left in left-to-right languages). + endToStart, + + /// The [Swipeable] can be swiped by dragging in the reading direction + /// (e.g., from left to right in left-to-right languages). + startToEnd, +} + +class Swipeable extends StatefulWidget { + /// Creates a widget that calls a function when swiped. + /// + /// The [key] argument must not be null because [Swipeable]s are commonly + /// used in lists and removed from the list when dismissed. Without keys, the + /// default behavior is to sync widgets based on their index in the list, + /// which means the item after the dismissed item would be synced with the + /// state of the dismissed item. Using keys causes the widgets to sync + /// according to their keys and avoids this pitfall. + const Swipeable({ + @required Key key, + @required this.child, + this.background, + this.secondaryBackground, + this.confirmSwipe, + this.onSwiped, + this.direction = SwipeDirection.horizontal, + this.dismissThresholds = const {}, + this.maxOffset = 0.4, + this.movementDuration = const Duration(milliseconds: 200), + this.crossAxisEndOffset = 0.0, + this.dragStartBehavior = DragStartBehavior.start, + }) : assert(key != null), + assert(secondaryBackground == null || background != null), + assert(dragStartBehavior != null), + super(key: key); + + /// The widget below this widget in the tree. + /// + /// {@macro flutter.widgets.child} + final Widget child; + + /// A widget that is stacked behind the child. If secondaryBackground is also + /// specified then this widget only appears when the child has been dragged + /// to the right. + final Widget background; + + /// A widget that is stacked behind the child and is exposed when the child + /// has been dragged to the left. It may only be specified when background + /// has also been specified. + final Widget secondaryBackground; + + /// Gives the app an opportunity to confirm or veto a pending dismissal. + /// + /// If the returned Future completes true, then this widget will be + /// dismissed, otherwise it will be moved back to its original location. + /// + /// If the returned Future completes to false or null the [onSwiped] + /// callback will not run. + final ConfirmSwipeCallback confirmSwipe; + + /// Called when the widget has been dismissed, after finishing resizing. + final SwipeDirectionCallback onSwiped; + + /// The direction in which the widget can be dismissed. + final SwipeDirection direction; + + /// The offset threshold the item has to be dragged in order to be considered + /// dismissed. + /// + /// Represented as a fraction, e.g. if it is 0.4 (the default), then the item + /// has to be dragged at least 40% towards one direction to be considered + /// dismissed. Clients can define different thresholds for each dismiss + /// direction. + /// + /// Flinging is treated as being equivalent to dragging almost to 1.0, so + /// flinging can dismiss an item past any threshold less than 1.0. + /// + /// Setting a threshold of 1.0 (or greater) prevents a drag in the given + /// [SwipeDirection] even if it would be allowed by the [direction] + /// property. + /// + /// See also: + /// + /// * [direction], which controls the directions in which the items can + /// be dismissed. + final Map dismissThresholds; + + /// The maximum horizontal offset the item can move to/ + /// + /// Represented as a fraction, e.g. if it is 0.4 (the default), then the + /// item can be moved at maximum 40% of item's width. + final double maxOffset; + + /// Defines the duration for card to dismiss or to come back to original position if not dismissed. + final Duration movementDuration; + + /// Defines the end offset across the main axis after the card is dismissed. + /// + /// If non-zero value is given then widget moves in cross direction depending on whether + /// it is positive or negative. + final double crossAxisEndOffset; + + /// Determines the way that drag start behavior is handled. + /// + /// If set to [DragStartBehavior.start], the drag gesture used to dismiss a + /// dismissible will begin upon the detection of a drag gesture. If set to + /// [DragStartBehavior.down] it will begin when a down event is first detected. + /// + /// In general, setting this to [DragStartBehavior.start] will make drag + /// animation smoother and setting it to [DragStartBehavior.down] will make + /// drag behavior feel slightly more reactive. + /// + /// By default, the drag start behavior is [DragStartBehavior.start]. + /// + /// See also: + /// + /// * [DragGestureRecognizer.dragStartBehavior], which gives an example for the different behaviors. + final DragStartBehavior dragStartBehavior; + + @override + _SwipeableState createState() => _SwipeableState(); +} + +class _SwipeableClipper extends CustomClipper { + _SwipeableClipper({ + @required this.moveAnimation, + }) : assert(moveAnimation != null), + super(reclip: moveAnimation); + + final Animation moveAnimation; + + @override + Rect getClip(Size size) { + final offset = moveAnimation.value.dx * size.width; + if (offset < 0) { + return Rect.fromLTRB(size.width + offset, 0.0, size.width, size.height); + } + return Rect.fromLTRB(0.0, 0.0, offset, size.height); + } + + @override + Rect getApproximateClipRect(Size size) => getClip(size); + + @override + bool shouldReclip(_SwipeableClipper oldClipper) { + return oldClipper.moveAnimation.value != moveAnimation.value; + } +} + +enum _FlingGestureKind { none, forward, reverse } + +class _SwipeableState extends State + with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { + @override + void initState() { + super.initState(); + _moveController = + AnimationController(duration: widget.movementDuration, vsync: this) + ..addStatusListener(_handleDismissStatusChanged); + _updateMoveAnimation(); + } + + AnimationController _moveController; + Animation _moveAnimation; + + double _dragExtent = 0.0; + bool _dragUnderway = false; + Size _sizePriorToCollapse; + + @override + bool get wantKeepAlive => _moveController?.isAnimating == true; + + @override + void dispose() { + _moveController.dispose(); + super.dispose(); + } + + SwipeDirection _extentToDirection(double extent) { + if (extent == 0.0) { + return null; + } + switch (Directionality.of(context)) { + case TextDirection.rtl: + return extent < 0 + ? SwipeDirection.startToEnd + : SwipeDirection.endToStart; + case TextDirection.ltr: + return extent > 0 + ? SwipeDirection.startToEnd + : SwipeDirection.endToStart; + } + assert(false); + return null; + } + + SwipeDirection get _SwipeDirection => _extentToDirection(_dragExtent); + + bool get _isActive { + return _dragUnderway || _moveController.isAnimating; + } + + double get _overallDragAxisExtent { + final size = context.size; + return size.width; + } + + void _handleDragStart(DragStartDetails details) { + _dragUnderway = true; + if (_moveController.isAnimating) { + _dragExtent = + _moveController.value * _overallDragAxisExtent * _dragExtent.sign; + _moveController.stop(); + } else { + _dragExtent = 0.0; + _moveController.value = 0.0; + } + setState(() { + _updateMoveAnimation(); + }); + } + + void _handleDragUpdate(DragUpdateDetails details) { + if (!_isActive || _moveController.isAnimating) { + return; + } + + final delta = details.primaryDelta; + final oldDragExtent = _dragExtent; + switch (widget.direction) { + case SwipeDirection.horizontal: + _dragExtent += delta; + break; + + case SwipeDirection.endToStart: + switch (Directionality.of(context)) { + case TextDirection.rtl: + if (_dragExtent + delta > 0) { + _dragExtent += delta; + } + break; + case TextDirection.ltr: + if (_dragExtent + delta < 0) { + _dragExtent += delta; + } + break; + } + break; + + case SwipeDirection.startToEnd: + switch (Directionality.of(context)) { + case TextDirection.rtl: + if (_dragExtent + delta < 0) { + _dragExtent += delta; + } + break; + case TextDirection.ltr: + if (_dragExtent + delta > 0) { + _dragExtent += delta; + } + break; + } + break; + } + if (oldDragExtent.sign != _dragExtent.sign) { + setState(() { + _updateMoveAnimation(); + }); + } + if (!_moveController.isAnimating) { + _moveController.value = _dragExtent.abs() / _overallDragAxisExtent; + } + } + + void _updateMoveAnimation() { + final end = _dragExtent.sign; + _moveAnimation = _moveController.drive( + Tween( + begin: Offset.zero, + end: Offset(widget.maxOffset * end, widget.crossAxisEndOffset), + ), + ); + } + + _FlingGestureKind _describeFlingGesture(Velocity velocity) { + assert(widget.direction != null); + if (_dragExtent == 0.0) { + // If it was a fling, then it was a fling that was let loose at the exact + // middle of the range (i.e. when there's no displacement). In that case, + // we assume that the user meant to fling it back to the center, as + // opposed to having wanted to drag it out one way, then fling it past the + // center and into and out the other side. + return _FlingGestureKind.none; + } + final vx = velocity.pixelsPerSecond.dx; + final vy = velocity.pixelsPerSecond.dy; + SwipeDirection flingDirection; + // Verify that the fling is in the generally right direction and fast enough. + if (vx.abs() - vy.abs() < _kMinFlingVelocityDelta || + vx.abs() < _kMinFlingVelocity) { + return _FlingGestureKind.none; + } + assert(vx != 0.0); + flingDirection = _extentToDirection(vx); + + assert(_SwipeDirection != null); + if (flingDirection == _SwipeDirection) { + return _FlingGestureKind.forward; + } + return _FlingGestureKind.reverse; + } + + Future _handleDragEnd(DragEndDetails details) async { + if (!_isActive || _moveController.isAnimating) { + return; + } + _dragUnderway = false; + if (_moveController.isCompleted && + await _confirmStartResizeAnimation() == true) { + _startResizeAnimation(); + return; + } + final flingVelocity = details.velocity.pixelsPerSecond.dx; + switch (_describeFlingGesture(details.velocity)) { + case _FlingGestureKind.forward: + assert(_dragExtent != 0.0); + assert(!_moveController.isDismissed); + if ((widget.dismissThresholds[_SwipeDirection] ?? _kDismissThreshold) >= + 1.0) { + await _moveController.reverse(); + break; + } + _dragExtent = flingVelocity.sign; + await _moveController.fling( + velocity: flingVelocity.abs() * _kFlingVelocityScale); + break; + case _FlingGestureKind.reverse: + assert(_dragExtent != 0.0); + assert(!_moveController.isDismissed); + _dragExtent = flingVelocity.sign; + await _moveController.fling( + velocity: -flingVelocity.abs() * _kFlingVelocityScale); + break; + case _FlingGestureKind.none: + if (!_moveController.isDismissed) { + // we already know it's not completed, we check that above + if (_moveController.value > + (widget.dismissThresholds[_SwipeDirection] ?? + _kDismissThreshold)) { + await _moveController.forward(); + } else { + await _moveController.reverse(); + } + } + break; + } + } + + Future _handleDismissStatusChanged(AnimationStatus status) async { + if (status == AnimationStatus.completed && !_dragUnderway) { + if (await _confirmStartResizeAnimation() == true) { + _startResizeAnimation(); + } else { + await _moveController.reverse(); + } + } + updateKeepAlive(); + } + + Future _confirmStartResizeAnimation() async { + if (widget.confirmSwipe != null) { + final direction = _SwipeDirection; + assert(direction != null); + return widget.confirmSwipe(direction); + } + return true; + } + + void _startResizeAnimation() { + assert(_moveController != null); + assert(_moveController.isCompleted); + assert(_sizePriorToCollapse == null); + _moveController.reverse(); + if (widget.onSwiped != null) { + final direction = _SwipeDirection; + assert(direction != null); + widget.onSwiped(direction); + } + } + + @override + Widget build(BuildContext context) { + super.build(context); // See AutomaticKeepAliveClientMixin. + + assert(debugCheckHasDirectionality(context)); + + var background = widget.background; + if (widget.secondaryBackground != null) { + final direction = _SwipeDirection; + if (direction == SwipeDirection.endToStart) { + background = widget.secondaryBackground; + } + } + + Widget content = SlideTransition( + position: _moveAnimation, + child: widget.child, + ); + + if (background != null) { + content = Stack(children: [ + if (!_moveAnimation.isDismissed) + Positioned.fill( + child: ClipRect( + clipper: _SwipeableClipper( + moveAnimation: _moveAnimation, + ), + child: background, + ), + ), + content, + ]); + } + // We are not swiping but we may be being dragging in widget.direction. + return GestureDetector( + onHorizontalDragStart: _handleDragStart, + onHorizontalDragUpdate: _handleDragUpdate, + onHorizontalDragEnd: _handleDragEnd, + behavior: HitTestBehavior.opaque, + child: content, + dragStartBehavior: widget.dragStartBehavior, + ); + } +} diff --git a/lib/views/chat.dart b/lib/views/chat.dart index 5bb4cbf..036ced2 100644 --- a/lib/views/chat.dart +++ b/lib/views/chat.dart @@ -351,6 +351,14 @@ class _ChatState extends State<_Chat> { inputFocus.requestFocus(); } + void replyBySwipeAction(Event replyTo) { + setState(() { + replyEvent = replyTo; + selectedEvents.clear(); + }); + inputFocus.requestFocus(); + } + void _scrollToEventId(String eventId, {BuildContext context}) async { var eventIndex = getFilteredEvents().indexWhere((e) => e.eventId == eventId); @@ -668,21 +676,40 @@ class _ChatState extends State<_Chat> { key: ValueKey(i - 1), index: i - 1, controller: _scrollController, - child: Message(filteredEvents[i - 1], - onAvatarTab: (Event event) { - sendController.text += - ' ${event.senderId}'; - }, - onSelect: (Event event) { - if (!event.redacted) { - if (selectedEvents - .contains(event)) { - setState( - () => selectedEvents - .remove(event), - ); - } else { - setState( + child: Swipeable( + key: ValueKey(i - 1), + background: Container( + color: Theme.of(context).primaryColor.withAlpha(100), + padding: EdgeInsets.symmetric(horizontal: 12.0), + alignment: Alignment.centerLeft, + child: Row( + children: [ + Icon(Icons.reply), + SizedBox(width: 2.0), + Text(L10n.of(context).reply) + ], + ), + ), + direction: SwipeDirection.startToEnd, + onSwiped: (direction) { + replyBySwipeAction( + filteredEvents[i - 1]); + }, + child: Message(filteredEvents[i - 1], + onAvatarTab: (Event event) { + sendController.text += + ' ${event.senderId}'; + }, + onSelect: (Event event) { + if (!event.redacted) { + if (selectedEvents + .contains(event)) { + setState( + () => selectedEvents + .remove(event), + ); + } else { + setState( () => selectedEvents.add(event), ); @@ -705,6 +732,7 @@ class _ChatState extends State<_Chat> { nextEvent: i >= 2 ? filteredEvents[i - 2] : null), + ), ); }); }, From 20efc3dbb6469692c1af645a98905245b5ae192f Mon Sep 17 00:00:00 2001 From: Inex Code Date: Sun, 4 Oct 2020 23:37:32 +0000 Subject: [PATCH 13/20] fix formatting --- lib/components/swipeable.dart | 2 +- lib/views/chat.dart | 47 +++++++++++++++++++---------------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/lib/components/swipeable.dart b/lib/components/swipeable.dart index 71543dc..f876c28 100644 --- a/lib/components/swipeable.dart +++ b/lib/components/swipeable.dart @@ -111,7 +111,7 @@ class Swipeable extends StatefulWidget { final Map dismissThresholds; /// The maximum horizontal offset the item can move to/ - /// + /// /// Represented as a fraction, e.g. if it is 0.4 (the default), then the /// item can be moved at maximum 40% of item's width. final double maxOffset; diff --git a/lib/views/chat.dart b/lib/views/chat.dart index 036ced2..f86fb6b 100644 --- a/lib/views/chat.dart +++ b/lib/views/chat.dart @@ -679,8 +679,11 @@ class _ChatState extends State<_Chat> { child: Swipeable( key: ValueKey(i - 1), background: Container( - color: Theme.of(context).primaryColor.withAlpha(100), - padding: EdgeInsets.symmetric(horizontal: 12.0), + color: Theme.of(context) + .primaryColor + .withAlpha(100), + padding: EdgeInsets.symmetric( + horizontal: 12.0), alignment: Alignment.centerLeft, child: Row( children: [ @@ -710,28 +713,28 @@ class _ChatState extends State<_Chat> { ); } else { setState( - () => - selectedEvents.add(event), + () => selectedEvents + .add(event), + ); + } + selectedEvents.sort( + (a, b) => a.originServerTs + .compareTo( + b.originServerTs), ); } - selectedEvents.sort( - (a, b) => a.originServerTs - .compareTo( - b.originServerTs), - ); - } - }, - scrollToEventId: (String eventId) => - _scrollToEventId(eventId, - context: context), - longPressSelect: - selectedEvents.isEmpty, - selected: selectedEvents - .contains(filteredEvents[i - 1]), - timeline: timeline, - nextEvent: i >= 2 - ? filteredEvents[i - 2] - : null), + }, + scrollToEventId: (String eventId) => + _scrollToEventId(eventId, + context: context), + longPressSelect: + selectedEvents.isEmpty, + selected: selectedEvents.contains( + filteredEvents[i - 1]), + timeline: timeline, + nextEvent: i >= 2 + ? filteredEvents[i - 2] + : null), ), ); }); From 3ee20eb07c285168d769aca6c9fa8210098a638e Mon Sep 17 00:00:00 2001 From: Inex Code Date: Mon, 5 Oct 2020 01:28:34 +0000 Subject: [PATCH 14/20] fix replying to wrong message when new message arrives during the gesture --- lib/views/chat.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/views/chat.dart b/lib/views/chat.dart index f86fb6b..9cce68f 100644 --- a/lib/views/chat.dart +++ b/lib/views/chat.dart @@ -677,7 +677,8 @@ class _ChatState extends State<_Chat> { index: i - 1, controller: _scrollController, child: Swipeable( - key: ValueKey(i - 1), + key: ValueKey( + filteredEvents[i - 1].eventId), background: Container( color: Theme.of(context) .primaryColor From de41700741a62d0d5ef1912e41de5e2bce82ee9a Mon Sep 17 00:00:00 2001 From: Inex Code Date: Tue, 6 Oct 2020 20:56:29 +0300 Subject: [PATCH 15/20] Add pointer type detection for Swipeable --- lib/components/swipeable.dart | 36 ++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/lib/components/swipeable.dart b/lib/components/swipeable.dart index f876c28..fd1a6f5 100644 --- a/lib/components/swipeable.dart +++ b/lib/components/swipeable.dart @@ -1,3 +1,4 @@ +import 'dart:ui'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -54,6 +55,11 @@ class Swipeable extends StatefulWidget { this.movementDuration = const Duration(milliseconds: 200), this.crossAxisEndOffset = 0.0, this.dragStartBehavior = DragStartBehavior.start, + this.allowedPointerKinds = const { + PointerDeviceKind.invertedStylus, + PointerDeviceKind.stylus, + PointerDeviceKind.touch + }, }) : assert(key != null), assert(secondaryBackground == null || background != null), assert(dragStartBehavior != null), @@ -125,6 +131,11 @@ class Swipeable extends StatefulWidget { /// it is positive or negative. final double crossAxisEndOffset; + /// Defines pointer types which are allowed to trigger swipe gesture. + /// + /// Defaults to {PointerDeviceKind.touch, PointerDeviceKind.invertedStylus, PointerDeviceKind.stylus} + final Set allowedPointerKinds; + /// Determines the way that drag start behavior is handled. /// /// If set to [DragStartBehavior.start], the drag gesture used to dismiss a @@ -192,6 +203,8 @@ class _SwipeableState extends State bool _dragUnderway = false; Size _sizePriorToCollapse; + bool _isTouch = true; + @override bool get wantKeepAlive => _moveController?.isAnimating == true; @@ -230,6 +243,12 @@ class _SwipeableState extends State return size.width; } + void _handlePointerDown(PointerDownEvent event) { + setState(() { + _isTouch = widget.allowedPointerKinds.contains(event.kind); + }); + } + void _handleDragStart(DragStartDetails details) { _dragUnderway = true; if (_moveController.isAnimating) { @@ -447,13 +466,16 @@ class _SwipeableState extends State ]); } // We are not swiping but we may be being dragging in widget.direction. - return GestureDetector( - onHorizontalDragStart: _handleDragStart, - onHorizontalDragUpdate: _handleDragUpdate, - onHorizontalDragEnd: _handleDragEnd, - behavior: HitTestBehavior.opaque, - child: content, - dragStartBehavior: widget.dragStartBehavior, + return Listener( + onPointerDown: _handlePointerDown, + child: GestureDetector( + onHorizontalDragStart: _isTouch ? _handleDragStart : null, + onHorizontalDragUpdate: _isTouch ? _handleDragUpdate : null, + onHorizontalDragEnd: _isTouch ? _handleDragEnd : null, + behavior: HitTestBehavior.opaque, + child: content, + dragStartBehavior: widget.dragStartBehavior, + ), ); } } From 84a4a2c0c416f0181e2c7a0a16caa363e85aca76 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Tue, 6 Oct 2020 18:53:15 +0000 Subject: [PATCH 16/20] Code deduplication --- lib/views/chat.dart | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/lib/views/chat.dart b/lib/views/chat.dart index 9cce68f..cdfd254 100644 --- a/lib/views/chat.dart +++ b/lib/views/chat.dart @@ -343,17 +343,9 @@ class _ChatState extends State<_Chat> { setState(() => selectedEvents.clear()); } - void replyAction() { + void replyAction({Event replyTo}) { setState(() { - replyEvent = selectedEvents.first; - selectedEvents.clear(); - }); - inputFocus.requestFocus(); - } - - void replyBySwipeAction(Event replyTo) { - setState(() { - replyEvent = replyTo; + replyEvent = replyTo ?? selectedEvents.first; selectedEvents.clear(); }); inputFocus.requestFocus(); @@ -696,8 +688,8 @@ class _ChatState extends State<_Chat> { ), direction: SwipeDirection.startToEnd, onSwiped: (direction) { - replyBySwipeAction( - filteredEvents[i - 1]); + replyAction( + replyTo: filteredEvents[i - 1]); }, child: Message(filteredEvents[i - 1], onAvatarTab: (Event event) { From 23936fa7f1fcafba35b7eb578accc01a29df953e Mon Sep 17 00:00:00 2001 From: Inex Code Date: Wed, 7 Oct 2020 11:20:50 +0000 Subject: [PATCH 17/20] Move Swipeable to separate package --- lib/components/swipeable.dart | 481 ---------------------------------- lib/views/chat.dart | 3 +- pubspec.lock | 7 + pubspec.yaml | 1 + 4 files changed, 10 insertions(+), 482 deletions(-) delete mode 100644 lib/components/swipeable.dart diff --git a/lib/components/swipeable.dart b/lib/components/swipeable.dart deleted file mode 100644 index fd1a6f5..0000000 --- a/lib/components/swipeable.dart +++ /dev/null @@ -1,481 +0,0 @@ -import 'dart:ui'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; - -const double _kMinFlingVelocity = 700.0; -const double _kMinFlingVelocityDelta = 400.0; -const double _kFlingVelocityScale = 1.0 / 300.0; -const double _kDismissThreshold = 0.4; - -/// Signature used by [Swipeable] to indicate that it has been swiped in -/// the given `direction`. -/// -/// Used by [Swipeable.onSwiped]. -typedef SwipeDirectionCallback = void Function(SwipeDirection direction); - -/// Signature used by [Swipeable] to give the application an opportunity to -/// confirm or veto a swipe gesture. -/// -/// Used by [Swipeable.confirmSwipe]. -typedef ConfirmSwipeCallback = Future Function(SwipeDirection direction); - -/// The direction in which a [Swipeable] can be swiped. -enum SwipeDirection { - /// The [Swipeable] can be swiped by dragging either left or right. - horizontal, - - /// The [Swipeable] can be swiped by dragging in the reverse of the - /// reading direction (e.g., from right to left in left-to-right languages). - endToStart, - - /// The [Swipeable] can be swiped by dragging in the reading direction - /// (e.g., from left to right in left-to-right languages). - startToEnd, -} - -class Swipeable extends StatefulWidget { - /// Creates a widget that calls a function when swiped. - /// - /// The [key] argument must not be null because [Swipeable]s are commonly - /// used in lists and removed from the list when dismissed. Without keys, the - /// default behavior is to sync widgets based on their index in the list, - /// which means the item after the dismissed item would be synced with the - /// state of the dismissed item. Using keys causes the widgets to sync - /// according to their keys and avoids this pitfall. - const Swipeable({ - @required Key key, - @required this.child, - this.background, - this.secondaryBackground, - this.confirmSwipe, - this.onSwiped, - this.direction = SwipeDirection.horizontal, - this.dismissThresholds = const {}, - this.maxOffset = 0.4, - this.movementDuration = const Duration(milliseconds: 200), - this.crossAxisEndOffset = 0.0, - this.dragStartBehavior = DragStartBehavior.start, - this.allowedPointerKinds = const { - PointerDeviceKind.invertedStylus, - PointerDeviceKind.stylus, - PointerDeviceKind.touch - }, - }) : assert(key != null), - assert(secondaryBackground == null || background != null), - assert(dragStartBehavior != null), - super(key: key); - - /// The widget below this widget in the tree. - /// - /// {@macro flutter.widgets.child} - final Widget child; - - /// A widget that is stacked behind the child. If secondaryBackground is also - /// specified then this widget only appears when the child has been dragged - /// to the right. - final Widget background; - - /// A widget that is stacked behind the child and is exposed when the child - /// has been dragged to the left. It may only be specified when background - /// has also been specified. - final Widget secondaryBackground; - - /// Gives the app an opportunity to confirm or veto a pending dismissal. - /// - /// If the returned Future completes true, then this widget will be - /// dismissed, otherwise it will be moved back to its original location. - /// - /// If the returned Future completes to false or null the [onSwiped] - /// callback will not run. - final ConfirmSwipeCallback confirmSwipe; - - /// Called when the widget has been dismissed, after finishing resizing. - final SwipeDirectionCallback onSwiped; - - /// The direction in which the widget can be dismissed. - final SwipeDirection direction; - - /// The offset threshold the item has to be dragged in order to be considered - /// dismissed. - /// - /// Represented as a fraction, e.g. if it is 0.4 (the default), then the item - /// has to be dragged at least 40% towards one direction to be considered - /// dismissed. Clients can define different thresholds for each dismiss - /// direction. - /// - /// Flinging is treated as being equivalent to dragging almost to 1.0, so - /// flinging can dismiss an item past any threshold less than 1.0. - /// - /// Setting a threshold of 1.0 (or greater) prevents a drag in the given - /// [SwipeDirection] even if it would be allowed by the [direction] - /// property. - /// - /// See also: - /// - /// * [direction], which controls the directions in which the items can - /// be dismissed. - final Map dismissThresholds; - - /// The maximum horizontal offset the item can move to/ - /// - /// Represented as a fraction, e.g. if it is 0.4 (the default), then the - /// item can be moved at maximum 40% of item's width. - final double maxOffset; - - /// Defines the duration for card to dismiss or to come back to original position if not dismissed. - final Duration movementDuration; - - /// Defines the end offset across the main axis after the card is dismissed. - /// - /// If non-zero value is given then widget moves in cross direction depending on whether - /// it is positive or negative. - final double crossAxisEndOffset; - - /// Defines pointer types which are allowed to trigger swipe gesture. - /// - /// Defaults to {PointerDeviceKind.touch, PointerDeviceKind.invertedStylus, PointerDeviceKind.stylus} - final Set allowedPointerKinds; - - /// Determines the way that drag start behavior is handled. - /// - /// If set to [DragStartBehavior.start], the drag gesture used to dismiss a - /// dismissible will begin upon the detection of a drag gesture. If set to - /// [DragStartBehavior.down] it will begin when a down event is first detected. - /// - /// In general, setting this to [DragStartBehavior.start] will make drag - /// animation smoother and setting it to [DragStartBehavior.down] will make - /// drag behavior feel slightly more reactive. - /// - /// By default, the drag start behavior is [DragStartBehavior.start]. - /// - /// See also: - /// - /// * [DragGestureRecognizer.dragStartBehavior], which gives an example for the different behaviors. - final DragStartBehavior dragStartBehavior; - - @override - _SwipeableState createState() => _SwipeableState(); -} - -class _SwipeableClipper extends CustomClipper { - _SwipeableClipper({ - @required this.moveAnimation, - }) : assert(moveAnimation != null), - super(reclip: moveAnimation); - - final Animation moveAnimation; - - @override - Rect getClip(Size size) { - final offset = moveAnimation.value.dx * size.width; - if (offset < 0) { - return Rect.fromLTRB(size.width + offset, 0.0, size.width, size.height); - } - return Rect.fromLTRB(0.0, 0.0, offset, size.height); - } - - @override - Rect getApproximateClipRect(Size size) => getClip(size); - - @override - bool shouldReclip(_SwipeableClipper oldClipper) { - return oldClipper.moveAnimation.value != moveAnimation.value; - } -} - -enum _FlingGestureKind { none, forward, reverse } - -class _SwipeableState extends State - with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { - @override - void initState() { - super.initState(); - _moveController = - AnimationController(duration: widget.movementDuration, vsync: this) - ..addStatusListener(_handleDismissStatusChanged); - _updateMoveAnimation(); - } - - AnimationController _moveController; - Animation _moveAnimation; - - double _dragExtent = 0.0; - bool _dragUnderway = false; - Size _sizePriorToCollapse; - - bool _isTouch = true; - - @override - bool get wantKeepAlive => _moveController?.isAnimating == true; - - @override - void dispose() { - _moveController.dispose(); - super.dispose(); - } - - SwipeDirection _extentToDirection(double extent) { - if (extent == 0.0) { - return null; - } - switch (Directionality.of(context)) { - case TextDirection.rtl: - return extent < 0 - ? SwipeDirection.startToEnd - : SwipeDirection.endToStart; - case TextDirection.ltr: - return extent > 0 - ? SwipeDirection.startToEnd - : SwipeDirection.endToStart; - } - assert(false); - return null; - } - - SwipeDirection get _SwipeDirection => _extentToDirection(_dragExtent); - - bool get _isActive { - return _dragUnderway || _moveController.isAnimating; - } - - double get _overallDragAxisExtent { - final size = context.size; - return size.width; - } - - void _handlePointerDown(PointerDownEvent event) { - setState(() { - _isTouch = widget.allowedPointerKinds.contains(event.kind); - }); - } - - void _handleDragStart(DragStartDetails details) { - _dragUnderway = true; - if (_moveController.isAnimating) { - _dragExtent = - _moveController.value * _overallDragAxisExtent * _dragExtent.sign; - _moveController.stop(); - } else { - _dragExtent = 0.0; - _moveController.value = 0.0; - } - setState(() { - _updateMoveAnimation(); - }); - } - - void _handleDragUpdate(DragUpdateDetails details) { - if (!_isActive || _moveController.isAnimating) { - return; - } - - final delta = details.primaryDelta; - final oldDragExtent = _dragExtent; - switch (widget.direction) { - case SwipeDirection.horizontal: - _dragExtent += delta; - break; - - case SwipeDirection.endToStart: - switch (Directionality.of(context)) { - case TextDirection.rtl: - if (_dragExtent + delta > 0) { - _dragExtent += delta; - } - break; - case TextDirection.ltr: - if (_dragExtent + delta < 0) { - _dragExtent += delta; - } - break; - } - break; - - case SwipeDirection.startToEnd: - switch (Directionality.of(context)) { - case TextDirection.rtl: - if (_dragExtent + delta < 0) { - _dragExtent += delta; - } - break; - case TextDirection.ltr: - if (_dragExtent + delta > 0) { - _dragExtent += delta; - } - break; - } - break; - } - if (oldDragExtent.sign != _dragExtent.sign) { - setState(() { - _updateMoveAnimation(); - }); - } - if (!_moveController.isAnimating) { - _moveController.value = _dragExtent.abs() / _overallDragAxisExtent; - } - } - - void _updateMoveAnimation() { - final end = _dragExtent.sign; - _moveAnimation = _moveController.drive( - Tween( - begin: Offset.zero, - end: Offset(widget.maxOffset * end, widget.crossAxisEndOffset), - ), - ); - } - - _FlingGestureKind _describeFlingGesture(Velocity velocity) { - assert(widget.direction != null); - if (_dragExtent == 0.0) { - // If it was a fling, then it was a fling that was let loose at the exact - // middle of the range (i.e. when there's no displacement). In that case, - // we assume that the user meant to fling it back to the center, as - // opposed to having wanted to drag it out one way, then fling it past the - // center and into and out the other side. - return _FlingGestureKind.none; - } - final vx = velocity.pixelsPerSecond.dx; - final vy = velocity.pixelsPerSecond.dy; - SwipeDirection flingDirection; - // Verify that the fling is in the generally right direction and fast enough. - if (vx.abs() - vy.abs() < _kMinFlingVelocityDelta || - vx.abs() < _kMinFlingVelocity) { - return _FlingGestureKind.none; - } - assert(vx != 0.0); - flingDirection = _extentToDirection(vx); - - assert(_SwipeDirection != null); - if (flingDirection == _SwipeDirection) { - return _FlingGestureKind.forward; - } - return _FlingGestureKind.reverse; - } - - Future _handleDragEnd(DragEndDetails details) async { - if (!_isActive || _moveController.isAnimating) { - return; - } - _dragUnderway = false; - if (_moveController.isCompleted && - await _confirmStartResizeAnimation() == true) { - _startResizeAnimation(); - return; - } - final flingVelocity = details.velocity.pixelsPerSecond.dx; - switch (_describeFlingGesture(details.velocity)) { - case _FlingGestureKind.forward: - assert(_dragExtent != 0.0); - assert(!_moveController.isDismissed); - if ((widget.dismissThresholds[_SwipeDirection] ?? _kDismissThreshold) >= - 1.0) { - await _moveController.reverse(); - break; - } - _dragExtent = flingVelocity.sign; - await _moveController.fling( - velocity: flingVelocity.abs() * _kFlingVelocityScale); - break; - case _FlingGestureKind.reverse: - assert(_dragExtent != 0.0); - assert(!_moveController.isDismissed); - _dragExtent = flingVelocity.sign; - await _moveController.fling( - velocity: -flingVelocity.abs() * _kFlingVelocityScale); - break; - case _FlingGestureKind.none: - if (!_moveController.isDismissed) { - // we already know it's not completed, we check that above - if (_moveController.value > - (widget.dismissThresholds[_SwipeDirection] ?? - _kDismissThreshold)) { - await _moveController.forward(); - } else { - await _moveController.reverse(); - } - } - break; - } - } - - Future _handleDismissStatusChanged(AnimationStatus status) async { - if (status == AnimationStatus.completed && !_dragUnderway) { - if (await _confirmStartResizeAnimation() == true) { - _startResizeAnimation(); - } else { - await _moveController.reverse(); - } - } - updateKeepAlive(); - } - - Future _confirmStartResizeAnimation() async { - if (widget.confirmSwipe != null) { - final direction = _SwipeDirection; - assert(direction != null); - return widget.confirmSwipe(direction); - } - return true; - } - - void _startResizeAnimation() { - assert(_moveController != null); - assert(_moveController.isCompleted); - assert(_sizePriorToCollapse == null); - _moveController.reverse(); - if (widget.onSwiped != null) { - final direction = _SwipeDirection; - assert(direction != null); - widget.onSwiped(direction); - } - } - - @override - Widget build(BuildContext context) { - super.build(context); // See AutomaticKeepAliveClientMixin. - - assert(debugCheckHasDirectionality(context)); - - var background = widget.background; - if (widget.secondaryBackground != null) { - final direction = _SwipeDirection; - if (direction == SwipeDirection.endToStart) { - background = widget.secondaryBackground; - } - } - - Widget content = SlideTransition( - position: _moveAnimation, - child: widget.child, - ); - - if (background != null) { - content = Stack(children: [ - if (!_moveAnimation.isDismissed) - Positioned.fill( - child: ClipRect( - clipper: _SwipeableClipper( - moveAnimation: _moveAnimation, - ), - child: background, - ), - ), - content, - ]); - } - // We are not swiping but we may be being dragging in widget.direction. - return Listener( - onPointerDown: _handlePointerDown, - child: GestureDetector( - onHorizontalDragStart: _isTouch ? _handleDragStart : null, - onHorizontalDragUpdate: _isTouch ? _handleDragUpdate : null, - onHorizontalDragEnd: _isTouch ? _handleDragEnd : null, - behavior: HitTestBehavior.opaque, - child: content, - dragStartBehavior: widget.dragStartBehavior, - ), - ); - } -} diff --git a/lib/views/chat.dart b/lib/views/chat.dart index cdfd254..b991841 100644 --- a/lib/views/chat.dart +++ b/lib/views/chat.dart @@ -28,6 +28,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:image_picker/image_picker.dart'; import 'package:pedantic/pedantic.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; +import 'package:swipe_to_action/swipe_to_action.dart'; import '../components/dialogs/send_file_dialog.dart'; import '../components/input_bar.dart'; @@ -687,7 +688,7 @@ class _ChatState extends State<_Chat> { ), ), direction: SwipeDirection.startToEnd, - onSwiped: (direction) { + onSwipe: (direction) { replyAction( replyTo: filteredEvents[i - 1]); }, diff --git a/pubspec.lock b/pubspec.lock index eaec670..c5876b0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -870,6 +870,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0-nullsafety.1" + swipe_to_action: + dependency: "direct main" + description: + name: swipe_to_action + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0" synchronized: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f7f6b5e..a22aeff 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -69,6 +69,7 @@ dependencies: ref: master flutter_blurhash: ^0.5.0 scroll_to_index: ^1.0.6 + swipe_to_action: ^0.1.0 dev_dependencies: flutter_test: From c97cb326f82c8ce633f452964a5c5bf3ef8f6b3f Mon Sep 17 00:00:00 2001 From: Inex Code Date: Wed, 14 Oct 2020 06:26:33 +0300 Subject: [PATCH 18/20] Add settings for swipe actions --- lib/components/matrix.dart | 13 +++ lib/l10n/intl_en.arb | 15 +++ lib/views/chat.dart | 158 ++++++++++++++++++++++---- lib/views/settings/settings_chat.dart | 89 +++++++++++++++ 4 files changed, 253 insertions(+), 22 deletions(-) diff --git a/lib/components/matrix.dart b/lib/components/matrix.dart index 3a9b8a2..27b2219 100644 --- a/lib/components/matrix.dart +++ b/lib/components/matrix.dart @@ -71,6 +71,9 @@ class MatrixState extends State { File wallpaper; bool renderHtml = false; + String swipeToEndAction; + String swipeToStartAction = 'reply'; + String jitsiInstance = 'https://meet.jit.si/'; void clean() async { @@ -283,6 +286,16 @@ class MatrixState extends State { store.getItem('chat.fluffy.renderHtml').then((final render) async { renderHtml = render == '1'; }); + store + .getItem('dev.inex.furrychat.swipeToEndAction') + .then((final action) async { + swipeToEndAction = action ?? swipeToEndAction; + }); + store + .getItem('dev.inex.furrychat.swipeToStartAction') + .then((final action) async { + swipeToStartAction = action ?? swipeToStartAction; + }); } if (kIsWeb) { onFocusSub = html.window.onFocus.listen((_) => webHasFocus = true); diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 588e861..caf9aa8 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -514,6 +514,11 @@ "type": "text", "placeholders": {} }, + "edit": "Edit", + "@edit": { + "type": "text", + "placeholders": {} + }, "editDisplayname": "Edit displayname", "@editDisplayname": { "type": "text", @@ -1439,6 +1444,16 @@ "type": "text", "placeholders": {} }, + "swipeToEndAction": "Swipe to right action", + "@swipeToEndAction": { + "type": "text", + "placeholders": {} + }, + "swipeToStartAction": "Swipe to left action", + "@swipeToStartAction": { + "type": "text", + "placeholders": {} + }, "donate": "Donate", "@donate": { "type": "text", diff --git a/lib/views/chat.dart b/lib/views/chat.dart index b991841..f989262 100644 --- a/lib/views/chat.dart +++ b/lib/views/chat.dart @@ -317,8 +317,10 @@ class _ChatState extends State<_Chat> { return true; } - void forwardEventsAction(BuildContext context) async { - if (selectedEvents.length == 1) { + void forwardEventsAction(BuildContext context, {Event event}) async { + if (event != null) { + Matrix.of(context).shareContent = event.content; + } else if (selectedEvents.length == 1) { Matrix.of(context).shareContent = selectedEvents.first.content; } else { Matrix.of(context).shareContent = { @@ -412,6 +414,128 @@ class _ChatState extends State<_Chat> { e.type != 'm.reaction') .toList(); + SwipeDirection _getSwipeDirection(Event event) { + var swipeToEndAction = Matrix.of(context).swipeToEndAction; + var swipeToStartAction = Matrix.of(context).swipeToStartAction; + var client = Matrix.of(context).client; + if (event.senderId != client.userID && swipeToEndAction == 'edit') { + swipeToEndAction = null; + } + if (event.senderId != client.userID && swipeToStartAction == 'edit') { + swipeToStartAction = null; + } + if (swipeToEndAction != null && swipeToStartAction != null) { + return SwipeDirection.horizontal; + } + if (swipeToEndAction != null) { + return SwipeDirection.startToEnd; + } + if (swipeToStartAction != null) { + return SwipeDirection.endToStart; + } + return null; + } + + Widget _getSwipeBackground(Event event, {bool isSecondary = false}) { + var alignToRight, action; + if (_getSwipeDirection(event) == SwipeDirection.horizontal) { + if (isSecondary) { + alignToRight = true; + action = Matrix.of(context).swipeToStartAction; + } else { + alignToRight = false; + action = Matrix.of(context).swipeToEndAction; + } + } else if (isSecondary) { + return null; + } else if (_getSwipeDirection(event) == SwipeDirection.endToStart) { + alignToRight = true; + action = Matrix.of(context).swipeToStartAction; + } else { + alignToRight = false; + action = Matrix.of(context).swipeToStartAction; + } + + switch (action) { + case 'reply': + return Container( + color: Theme.of(context).primaryColor.withAlpha(100), + padding: EdgeInsets.symmetric(horizontal: 12.0), + child: Row( + mainAxisAlignment: + alignToRight ? MainAxisAlignment.end : MainAxisAlignment.start, + children: [ + Icon(Icons.reply_outlined), + SizedBox(width: 2.0), + Text(L10n.of(context).reply) + ], + ), + ); + case 'forward': + return Container( + color: Theme.of(context).primaryColor.withAlpha(100), + padding: EdgeInsets.symmetric(horizontal: 12.0), + child: Row( + mainAxisAlignment: + alignToRight ? MainAxisAlignment.end : MainAxisAlignment.start, + children: [ + Icon(Icons.forward_outlined), + SizedBox(width: 2.0), + Text(L10n.of(context).forward) + ], + ), + ); + case 'edit': + return Container( + color: Theme.of(context).primaryColor.withAlpha(100), + padding: EdgeInsets.symmetric(horizontal: 12.0), + child: Row( + mainAxisAlignment: + alignToRight ? MainAxisAlignment.end : MainAxisAlignment.start, + children: [ + Icon(Icons.edit_outlined), + SizedBox(width: 2.0), + Text(L10n.of(context).edit) + ], + ), + ); + default: + return Container( + color: Theme.of(context).primaryColor.withAlpha(100), + ); + } + } + + void _handleSwipe(SwipeDirection direction, Event event) { + var action; + if (direction == SwipeDirection.endToStart) { + action = Matrix.of(context).swipeToStartAction; + } else { + action = Matrix.of(context).swipeToEndAction; + } + + switch (action) { + case 'reply': + replyAction(replyTo: event); + break; + case 'forward': + forwardEventsAction(context, event: event); + break; + case 'edit': + setState(() { + editEvent = event; + sendController.text = editEvent + .getDisplayEvent(timeline) + .getLocalizedBody(MatrixLocals(L10n.of(context)), + withSenderNamePrefix: false, hideReply: true); + selectedEvents.clear(); + }); + inputFocus.requestFocus(); + break; + default: + } + } + @override Widget build(BuildContext context) { matrix = Matrix.of(context); @@ -672,26 +796,16 @@ class _ChatState extends State<_Chat> { child: Swipeable( key: ValueKey( filteredEvents[i - 1].eventId), - background: Container( - color: Theme.of(context) - .primaryColor - .withAlpha(100), - padding: EdgeInsets.symmetric( - horizontal: 12.0), - alignment: Alignment.centerLeft, - child: Row( - children: [ - Icon(Icons.reply), - SizedBox(width: 2.0), - Text(L10n.of(context).reply) - ], - ), - ), - direction: SwipeDirection.startToEnd, - onSwipe: (direction) { - replyAction( - replyTo: filteredEvents[i - 1]); - }, + background: _getSwipeBackground( + filteredEvents[i - 1]), + secondaryBackground: + _getSwipeBackground( + filteredEvents[i - 1], + isSecondary: true), + direction: _getSwipeDirection( + filteredEvents[i - 1]), + onSwipe: (direction) => _handleSwipe( + direction, filteredEvents[i - 1]), child: Message(filteredEvents[i - 1], onAvatarTab: (Event event) { sendController.text += diff --git a/lib/views/settings/settings_chat.dart b/lib/views/settings/settings_chat.dart index d68d346..43c6268 100644 --- a/lib/views/settings/settings_chat.dart +++ b/lib/views/settings/settings_chat.dart @@ -22,6 +22,74 @@ class ChatSettings extends StatefulWidget { } class _ChatSettingsState extends State { + String _getActionDescription(String action) { + switch (action) { + case 'reply': + return L10n.of(context).reply; + case 'forward': + return L10n.of(context).forward; + case 'edit': + return L10n.of(context).edit; + default: + return L10n.of(context).none; + } + } + + void _changeSwipeAction(bool isToEnd, String action) async { + if (isToEnd) { + Matrix.of(context).swipeToEndAction = action; + await Matrix.of(context) + .store + .setItem('chat.fluffy.swipeToEndAction', action); + setState(() => null); + } else { + Matrix.of(context).swipeToStartAction = action; + await Matrix.of(context) + .store + .setItem('chat.fluffy.swipeToStartAction', action); + setState(() => null); + } + } + + Widget _swipeActionChooser(BuildContext context, bool isToEnd) { + return ListView( + children: [ + ListTile( + title: Text(L10n.of(context).none), + leading: Icon(Icons.clear_outlined), + onTap: () { + _changeSwipeAction(isToEnd, null); + Navigator.of(context).pop(); + }, + ), + ListTile( + title: Text(L10n.of(context).reply), + leading: Icon(Icons.reply_outlined), + onTap: () { + _changeSwipeAction(isToEnd, 'reply'); + Navigator.of(context).pop(); + }, + ), + ListTile( + title: Text(L10n.of(context).forward), + leading: Icon(Icons.forward_outlined), + onTap: () { + _changeSwipeAction(isToEnd, 'forward'); + Navigator.of(context).pop(); + }, + ), + ListTile( + title: Text(L10n.of(context).edit), + leading: Icon(Icons.edit_outlined), + onTap: () { + _changeSwipeAction(isToEnd, 'edit'); + Navigator.of(context).pop(); + }, + ), + ], + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -42,6 +110,27 @@ class _ChatSettingsState extends State { }, ), ), + Divider(thickness: 1), + ListTile( + title: Text(L10n.of(context).swipeToEndAction), + onTap: () => showModalBottomSheet( + context: context, + builder: (BuildContext context) => + _swipeActionChooser(context, true), + ), + subtitle: Text( + _getActionDescription(Matrix.of(context).swipeToEndAction)), + ), + ListTile( + title: Text(L10n.of(context).swipeToStartAction), + onTap: () => showModalBottomSheet( + context: context, + builder: (BuildContext context) => + _swipeActionChooser(context, false), + ), + subtitle: Text( + _getActionDescription(Matrix.of(context).swipeToStartAction)), + ), ], ), ); From b061c4471a927388d8a0114a1f30e7d01f9eef40 Mon Sep 17 00:00:00 2001 From: Inex Code Date: Wed, 14 Oct 2020 03:37:00 +0000 Subject: [PATCH 19/20] Update russian translation --- lib/l10n/intl_ru.arb | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index d8960c1..7c4893f 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -114,6 +114,11 @@ "type": "text", "placeholders": {} }, + "avatar": "Аватар", + "@avatar": { + "type": "text", + "placeholders": {} + }, "avatarHasBeenChanged": "Аватар был изменён", "@avatarHasBeenChanged": { "type": "text", @@ -197,6 +202,11 @@ "displayname": {} } }, + "changeThePassword": "Сменить пароль", + "@changeThePassword": { + "type": "text", + "placeholders": {} + }, "changedTheGuestAccessRules": "{username} изменил(а) правила гостевого доступа", "@changedTheGuestAccessRules": { "type": "text", @@ -509,6 +519,11 @@ "type": "text", "placeholders": {} }, + "edit": "Редактировать", + "@edit": { + "type": "text", + "placeholders": {} + }, "editDisplayname": "Отображаемое имя", "@editDisplayname": { "type": "text", @@ -681,11 +696,21 @@ "type": "text", "placeholders": {} }, + "homeserver": "Сервер Matrix", + "@homeserver": { + "type": "text", + "placeholders": {} + }, "homeserverIsNotCompatible": "Несовместимый сервер Matrix", "@homeserverIsNotCompatible": { "type": "text", "placeholders": {} }, + "homeserverOrMXID": "Сервер или полное имя пользователя", + "@homeserverOrMXID": { + "type": "text", + "placeholders": {} + }, "id": "ID", "@id": { "type": "text", @@ -1372,6 +1397,16 @@ "type": "text", "placeholders": {} }, + "swipeToEndAction": "Действие по жесту вправо", + "@swipeToEndAction": { + "type": "text", + "placeholders": {} + }, + "swipeToStartAction": "Действие по жесту влево", + "@swipeToStartAction": { + "type": "text", + "placeholders": {} + }, "systemTheme": "Системная", "@systemTheme": { "type": "text", From 787add88ae9b6f07319aaf1beedbef6d35f86d2f Mon Sep 17 00:00:00 2001 From: Inex Code Date: Wed, 14 Oct 2020 03:40:41 +0000 Subject: [PATCH 20/20] Update readme --- README.md | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index d489b0c..5c3a6c5 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ An experimental fork of FluffyChat. # Changes from FluffyChat + * Swipe to reply (or forward/edit) * Reworked auth flow * Removed Sentry * Double check of .well-known @@ -36,8 +37,8 @@ An experimental fork of FluffyChat. 2. Clone the repo: ``` -git clone --recurse-submodules https://gitlab.com/ChristianPauly/fluffychat-flutter -cd fluffychat-flutter +git clone --recurse-submodules https://github.com/innereq/FurryChat.git +cd FurryChat ``` 3. Choose your target platform below and enable support for it. @@ -81,13 +82,6 @@ flutter build windows --release flutter build macos --release ``` - -### Docker - -Don't even ask. - -`docker run -ti --privileged -v /dev/bus/usb:/dev/bus/usb -v ${PWD}:/build -v /home/inex/.pub-cache:/home/inex/.pub-cache -v /home/inex/flutter:/home/inex/flutter -d flutter-fluffy:1.0` - ## How to add translations for your language You can use Weblate to translate the app to your language: