diff --git a/README.md b/README.md index 1227016..d99478a 100644 --- a/README.md +++ b/README.md @@ -38,13 +38,26 @@ npm run build npm run lint ``` -### Customize configuration +### Customize build configuration See [Configuration Reference](https://cli.vuejs.org/config/). ## Theming +You can do simple theming by setting values in the configuration file, see below. -# Sticker short codes - To enable sticker short codes, follow these steps: +## Configuration file +The app loads runtime configutation from the server at "./config.json" and merges that with the default values in "assets/config.json". + The following values can be set via the config file: + +* **logo** - An url or base64-encoded image data url that represents the app logotype. +* **accentColor** - The accent color of the app UI. +* **show_status_messages** - Whether to show only user joins/leaves and display name updates, or the full range of room status updates. Possible values are "never" (only the above), "moderators" (moderators will see all status updates) or "always" (everyone will see all status updates). Defaults to "always". + + +### Sticker short codes - To enable sticker short codes, follow these steps: * Run the "create sticker config" script using "npm run create-sticker-config " * Insert the resulting config blob into the "shortCodeStickers" value of the config file (assets/config.json) * Rearrange order of sticker packs by editing the config blob above. + +### Attributions +Sounds from [Notification Sounds](https://notificationsounds.com) \ No newline at end of file diff --git a/src/App.vue b/src/App.vue index d8e2ad3..a3fea3a 100644 --- a/src/App.vue +++ b/src/App.vue @@ -145,10 +145,10 @@ export default { }, favicon() { - var favicon = 'favicon.ico'; + var favicon = this.$config.logo ? this.$config.logo : 'favicon.ico'; if (this.$route.meta.includeFavicon) { if (this.$matrix.currentRoom) { - favicon = this.$matrix.currentRoom.avatar || 'favicon.ico'; + favicon = this.$matrix.currentRoom.avatar || favicon; } } return favicon; diff --git a/src/assets/config.json b/src/assets/config.json index abc13bb..488b681 100644 --- a/src/assets/config.json +++ b/src/assets/config.json @@ -10,6 +10,8 @@ "defaultServer": "https://neo.keanu.im", "identityServer_unset": "", "rtl": false, + "accentColor_unset": "", + "logo_unset": "", "analytics": [ { "enabled": true, @@ -42,5 +44,7 @@ } ], "experimental_voice_mode": true, - "experimental_read_only_room": true + "experimental_read_only_room": true, + "experimental_public_room": true, + "show_status_messages": "never" } \ No newline at end of file diff --git a/src/assets/css/chat.scss b/src/assets/css/chat.scss index 873f015..deb30e5 100644 --- a/src/assets/css/chat.scss +++ b/src/assets/css/chat.scss @@ -356,6 +356,40 @@ body { .quick-reaction-container .emoji { display: inline; } + + .seen-by-container { + display: flex; + align-items: center; + justify-content: flex-end; + height: 16px; + .clickable { + display: flex; + height: 16px; + } + div { + height: 16px; + } + margin-top: 3px; + .more { + margin-right: 10px; + color: #444444; + font-size: 12px; + } + .seen-by-user { + width: 16px !important; + height: 16px !important; + margin-left: -5px !important; + vertical-align: top; + } + .list-enter-active, + .list-leave-active { + transition: all 1s; + } + .list-enter, .list-leave-to /* .list-leave-active below version 2.1.8 */ { + opacity: 0; + transform: translateX(24px); + } + } } .messageIn { @@ -1242,6 +1276,26 @@ body { } text-transform: none !important; } + + .room-option { + .v-input { + margin: 0px; + } + } + + .option-warning { + background: linear-gradient(0deg, #FFF3F3, #FFF3F3), #FFFBED; + border-radius: 8px; + padding: 18px; + font-family: 'Inter', sans-serif; + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 17px; + .v-icon { + margin-right: 16px; + } + } } .room-link .v-input__slot::before { diff --git a/src/assets/icons/ic_security-shield.vue b/src/assets/icons/ic_security-shield.vue new file mode 100644 index 0000000..1a75c57 --- /dev/null +++ b/src/assets/icons/ic_security-shield.vue @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/src/assets/icons/ic_warning.vue b/src/assets/icons/ic_warning.vue new file mode 100644 index 0000000..6250f59 --- /dev/null +++ b/src/assets/icons/ic_warning.vue @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/src/assets/sounds/record_stop.mp3 b/src/assets/sounds/record_stop.mp3 new file mode 100644 index 0000000..3c937aa Binary files /dev/null and b/src/assets/sounds/record_stop.mp3 differ diff --git a/src/assets/translations/bo.json b/src/assets/translations/bo.json index 9febcb8..f3cc3c7 100644 --- a/src/assets/translations/bo.json +++ b/src/assets/translations/bo.json @@ -87,7 +87,7 @@ "name_room": "ཁ་བརྡ་ཁང་གི་མིང་།", "room_topic": "གལ་ཏེ་འདོད་པ་ཡོད་ན། ཚོགས་པའི་སྐོར་གྱི་འགྲེལ་བཤད་ཐུང་ངུ་ཞིག་འབྲི་ཆོག", "create": "བཟོས།", - "colon_not_allowed": ": རྟགས་འདི་མི་ཆོག", + "colon_not_allowed": ": རྟགས་འདི་བཀོལ་མི་ཆོག", "options": "གདམ་ག", "room_name_limit_error_msg": "ཡིག་འབྲུ་གྲངས་༥༠ ལས་བརྒལ་མི་ཆོག" }, @@ -214,7 +214,8 @@ "user_was_banned": "{སྤྱོད་མཁན}་འདི་ཁ་བརྡའི་ཁོངས་ནས་བཀག་སྡོམ་བྱས་ཏེ་སྒོར་ཕུད་ཟིན།", "user_was_banned_by_you": "ཁྱེད་ཀྱིས་{སྤྱོད་མཁན} ་འདི་ཁ་བརྡའི་ཁོངས་ནས་བཀག་སྡོམ་བྱས་ཏེ་སྒོར་ཕུད་ཟིན།", "time_ago": "དེ་རིང་། | ཁ་སང་། | ཉིན་གྲངས་{count} གོང་།", - "outgoing_message_deleted_text": "ཁྱེད་ཀྱིས་ཆ་འཕྲིན་འདི་བསུབས་སོང་།" + "outgoing_message_deleted_text": "ཁྱེད་ཀྱིས་ཆ་འཕྲིན་འདི་བསུབས་སོང་།", + "reaction_count_more": "{reactionCount} མང་བ།" }, "power_level": { "moderator": "མདོ་འཛིན་པ།", @@ -289,7 +290,11 @@ "global": { "save": "ཉར་ཚགས།", "password_didnot_match": "གསང་ཚིག་མཐུན་གྱི་མི་འདུག", - "password_hint": "ཉུང་མཐར་ཡང་ཡིག་འབྲུ་༡༢་དགོས་ལ། དེའི་ནང་དུ་ཨང་གྲངས་གཅིག་དང་། ཡིག་ཆེན་གཅིག ཡིག་ཆུང་གཅིག་ངེས་པར་དུ་ཚང་དགོས།" + "password_hint": "ཉུང་མཐར་ཡང་ཡིག་འབྲུ་༡༢་དགོས་ལ། དེའི་ནང་དུ་ཨང་གྲངས་གཅིག་དང་། ཡིག་ཆེན་གཅིག ཡིག་ཆུང་གཅིག་ངེས་པར་དུ་ཚང་དགོས།", + "add_reaction": "ཡ་ལན་ཁ་སྣོན།", + "click_to_remove": "བསྣུན་ཏེ་མེད་པར་བཟོས།", + "show_less": "ཉུང་བ་སྟོན།", + "show_more": "མང་བ་སྟོན།" }, "logout": { "confirm_text": "ཁྱེད་རང་ཁ་བརྡ་ཁང་ནས་ཕྱི་རུ་ཐོན་རྒྱུ་ཡིན་ནམ།" @@ -299,20 +304,20 @@ "answer_label_1": "དྲིས་ལན། *", "tip_title": "ཆེད་ལས་གསལ་འདེབས།", "tip_text": "ཚོགས་མི་ཚོས་དྲིས་ལན་བཏབ་ཚར་ན་འདེམས་ཤོག་གི་གྲུབ་འབྲས་མཐོང་ཐུབ། དྲིས་ལན་བཏབ་ཚར་རྗེས་འདེམས་ཤོག་ཁ་བརྒྱབ་སྟེ་ཁ་བརྡ་ཁང་གི་ཚོགས་མི་ཚང་མར་འདེམས་ཤོག་གི་གྲུབ་འབྲས་སྟོན།", - "create_poll_menu_option": "བསམ་ཚུལ་བསྡུ་ལེན་གྱི་འདེམས་ཤོག་བཟོས།", + "create_poll_menu_option": "འདེམས་ཤོག་བཟོས།", "poll_status_closed": "འདེམས་ཤོག་སྒོ་བརྒྱབ་ཟིན།", "poll_status_open_not_voted": "འདེམས་ཤོག་སྒོ་ཕྱེ་ཡོད། -འདེམས་ཤོག་འཕངས་ཏེ་གྲུབ་འབྲས་ལ་གཟིགས།", "close_poll": "འདེམས་ཤོག་སྒོ་རྒྱོབ།", "poll_submit": "ཡར་སྤྲོད།", "num_answered": "དྲིས་ལན{count}", - "creating": "བསམ་ཚུལ་བསྡུ་ལེན་གྱི་འདེམས་ཤོག་བཟོ་བཞིན་པ།", - "poll_disclosed": "སྒོ་འབྱེད- མིག་སྔའི་མཇུག་འབྲས་ག་དུས་ཡིན་ཡང་སྟོན་གྱི་ཡོད།", - "poll_undisclosed": "སྒོ་བརྒྱབ - འདེམས་ཤོག་སྒོ་བརྒྱབ་ཚེ་སྤྱོད་མཁན་གྱིས་གྲུབ་འབྲས་མཐོང་ངེས།", + "creating": "འདེམས་ཤོག་བཟོ་བཞིན་པ།", + "poll_disclosed": "འགོ་བཙུགས་ཚར- མིག་སྔའི་མཇུག་འབྲས་ག་དུས་ཡིན་ཡང་སྟོན་གྱི་ཡོད།", + "poll_undisclosed": "མཇུག་བསྒྲིལ། - འདེམས་ཤོག་མཇུག་བསྒྲིལ་སྐབས་སྤྱོད་མཁན་གྱིས་གྲུབ་འབྲས་མཐོང་ངེས།", "question_label": "ཁྱེད་ཀྱི་དྲི་བ་དྲིས། *", "question_required": "ཁྱེད་ཀྱིས་དྲི་བ་ཞིག་སྐོང་དགོས།", "poll_status_disclosed": "འདེམས་ཤོག་སྒོ་བརྒྱབ་ན་གྲུབ་འབྲས་སྟོན་ངེས།", "poll_status_open": "འདེམས་ཤོག་སྒོ་ཕྱེ་ཡོད།", - "title": "བསམ་ཚུལ་བསྡུ་ལེན་གྱི་འདེམས་ཤོག་གསར་པ་ཞིག་བཟོས།", + "title": "འདེམས་ཤོག་གསར་པ་ཞིག་བཟོས།", "create": "འདོན་སྤེལ།", "add_answer": "དྲིས་ལན་ཁ་སྣོན།", "answer_label_n": "དྲིས་ལན།", diff --git a/src/assets/translations/en.json b/src/assets/translations/en.json index 4e07c04..cc8c121 100644 --- a/src/assets/translations/en.json +++ b/src/assets/translations/en.json @@ -58,6 +58,7 @@ "file_prefix": "File: ", "edited": "(edited)", "download_progress": "{percentage}% downloaded", + "upload_file_too_large": "File is too large to upload!", "upload_progress": "Uploaded {count}", "upload_progress_with_total": "Uploaded {count} of {total}", "user_changed_room_history": "{user} made room history {type}", @@ -88,7 +89,8 @@ "outgoing_message_deleted_text": "You deleted this message.", "incoming_message_deleted_text": "This message was deleted.", "not_allowed_to_send": "Only admins and moderators are allowed to send to the room", - "reaction_count_more": "{reactionCount} more" + "reaction_count_more": "{reactionCount} more", + "seen_by": "Seen by no members | Seen by 1 member | Seen by {count} members" }, "room": { "invitations": "You have no invitations | You have 1 invitation | You have {count} invitations", @@ -113,7 +115,8 @@ "join_public": "Anyone can join by opening this link: {link}.", "join_invite": "Only people you invite can join.", "info_permissions": "You can change ‘join permissions’ at any time in the room settings.", - "got_it": "Got it" + "got_it": "Got it", + "no_past_messages": "Welcome! For your security, past messages are not available." }, "new_room": { "new_room": "New Room", @@ -265,7 +268,9 @@ "voice_mode_info": "Switches the chat interface to a 'listen and record' mode", "download_chat": "Download chat", "read_only_room": "Read only room", - "read_only_room_info": "Only admins and moderators are allowed to send to the room" + "read_only_room_info": "Only admins and moderators are allowed to send to the room", + "make_public": "Make Public", + "make_public_warning": "warning: Full message history will be visible to new participants" }, "room_info_sheet": { "this_room": "This room", diff --git a/src/assets/translations/pt_BR.json b/src/assets/translations/pt_BR.json index f6abd0f..a4702e1 100644 --- a/src/assets/translations/pt_BR.json +++ b/src/assets/translations/pt_BR.json @@ -6,7 +6,11 @@ "global": { "save": "Salvar", "password_didnot_match": "A senha não coincidiu", - "password_hint": "Mínimo de 12 caracteres contendo pelo menos um número, uma maiúscula e uma minúscula" + "password_hint": "Mínimo de 12 caracteres contendo pelo menos um número, uma maiúscula e uma minúscula", + "show_less": "Mostrar menos", + "show_more": "Mostrar mais", + "add_reaction": "Adicionar reação", + "click_to_remove": "Clique para remover" }, "invite": { "title": "Adiciona amigos", @@ -96,7 +100,8 @@ "user_was_kicked_you": "Você foi expulso do chat.", "user_was_banned": "{user} foi expulso e banido do chat.", "user_was_kicked": "{user} foi expulso do chat.", - "user_was_banned_you": "Você foi expulso e banido do chat." + "user_was_banned_you": "Você foi expulso e banido do chat.", + "reaction_count_more": "{reactionCount} mais" }, "room": { "members": "sem membros | 1 membro | {count} membros", diff --git a/src/assets/translations/zh_Hans.json b/src/assets/translations/zh_Hans.json index fe69635..4c767d4 100644 --- a/src/assets/translations/zh_Hans.json +++ b/src/assets/translations/zh_Hans.json @@ -30,7 +30,16 @@ "permissions": "加入权限", "created_by": "由 {user} 创建", "copy_link": "复制邀请链接", - "scan_code": "扫一扫加入聊天室" + "scan_code": "扫一扫加入聊天室", + "user_admin": "管理员", + "voice_mode": "语音模块", + "voice_mode_info": "将聊天界面切换到“收听和录音”模式", + "download_chat": "下载聊天", + "read_only_room": "只读聊天室", + "read_only_room_info": "只允许管理员和版主发送到聊天室", + "export_room": "导出聊天", + "user_moderator": "版主", + "experimental_features": "实验功能" }, "leave": { "leave": "离开", @@ -51,7 +60,15 @@ "username": "用户名 (如: marta)", "create_room": "注册并创建聊天室", "or": "或者", - "invalid_message": "用户名或密码无效" + "invalid_message": "用户名或密码无效", + "resend_verification": "重新发送验证邮件", + "sent_verification": "一封电子邮件已发送至 {email}。 请使用您的常用电子邮件客户端来验证地址。", + "no_supported_flow": "该应用程序无法登录到给出的服务器", + "send_verification": "发送验证邮件", + "email_not_valid": "电子邮件地址无效", + "terms": "主服务器要求您查看并接受以下政策:", + "accept_terms": "接受", + "email": "您需要验证您的电子邮件地址" }, "device_list": { "title": "设备", @@ -65,10 +82,14 @@ "room_list_rooms": "聊天室", "room_list_invites": "邀请", "purge_failed": "删除聊天室失败了!", - "purge_removing_members": "移除成员", - "purge_redacting_events": "编辑事件", + "purge_removing_members": "移除成员({count} 个,共 {total} 个)", + "purge_redacting_events": "编辑事件({count} 个,共 {total} 个)", "purge_set_room_state": "设置聊天室状态", - "invitations": "您没有邀请 | 您有 1 个邀请 | 您有 {count} 个邀请" + "invitations": "您没有邀请 | 您有 1 个邀请 | 您有 {count} 个邀请", + "unseen_messages": "你没有任何未读信息 | 您有 1 条未读信息 | 您有 {count} 条未读信息", + "room_list_new_messages": "{count} 条新消息", + "room_name_required": "聊天室名称必填", + "room_topic_required": "需要聊天室描述" }, "message": { "you": "您", @@ -120,7 +141,8 @@ "time_ago": "今天| 昨天 | {count} 天前", "outgoing_message_deleted_text": "你删除了这条信息。", "incoming_message_deleted_text": "这条信息已删除。", - "not_allowed_to_send": "只允许管理员和版主发送到聊天室" + "not_allowed_to_send": "只允许管理员和版主发送到聊天室", + "reaction_count_more": "{reactionCount} 更多" }, "menu": { "login": "登录", @@ -191,7 +213,11 @@ "enter_room": "加入聊天室", "status_logging_in": "正在登录中...", "status_joining": "正在加入聊天室...", - "join_failed": "加入聊天室失败。" + "join_failed": "加入聊天室失败。", + "title_user": "欢迎您被邀请聊天", + "join_user": "开始聊天", + "enter_room_user": "开始聊天", + "choose_name": "选择要使用的名称" }, "profile": { "display_name": "显示名称", @@ -207,7 +233,8 @@ "set_language": "设置您的语言", "language_description": "Convene 提供多种语言.", "dont_see_yours": "看不到你的?", - "tell_us": "告诉我们。" + "tell_us": "告诉我们。", + "display_name_required": "显示名称是必需的" }, "new_room": { "status_avatar": "正在上传头像:{count}", @@ -223,12 +250,15 @@ "join_permissions_info": "这些权限决定了人们如何加入聊天室以及邀请其他人的难易程度。 你可以随时更改它们。", "set_join_permissions": "设置加入权限", "join_permissions": "加入权限", - "name_room": "命名聊天室", + "name_room": "聊天室名称", "next": "下一步", "done": "完毕", "new_room": "新的聊天室", "room_topic": "如果您愿意,请添加说明", - "create": "创建" + "create": "创建", + "colon_not_allowed": "冒号是不允许的", + "options": "选项", + "room_name_limit_error_msg": "最多允许 50 个字符" }, "room_welcome": { "got_it": "知道了", @@ -242,7 +272,7 @@ "encrypted": "信息是端到端加密的。" }, "profile_info_popup": { - "new_room": "+ 新的聊天室", + "new_room": "新的聊天室", "powered_by": "这个聊天室由 {product} 提供支持。 在 {productLink} 了解更多信息或继续创建另一个聊天室!", "want_more": "想要更多?", "logout": "退出登录", @@ -260,6 +290,47 @@ "global": { "save": "保存", "password_didnot_match": "密码不匹配", - "password_hint": "至少 12 个字符,包含至少一个数字、一个大写字母和一个小写字母。" + "password_hint": "至少 12 个字符,包含至少一个数字、一个大写字母和一个小写字母", + "show_less": "显示较少", + "show_more": "展示更多", + "add_reaction": "添加反应", + "click_to_remove": "点击删除" + }, + "logout": { + "confirm_text": "您确定要注销吗?" + }, + "poll_create": { + "title": "创建新投票", + "create": "发布", + "creating": "创建投票", + "poll_disclosed": "打开 - 当前结果始终显示。", + "answer_label_1": "答案*", + "answer_label_n": "答案", + "please_complete": "请完成", + "tip_title": "专业提示", + "tip_text": "成员回答后将看到投票结果。 完成后关闭投票,向聊天室里的每个人展示结果。", + "poll_status_open_not_voted": "投票已开始 - 投票以查看结果", + "poll_status_open": "投票已开始", + "view_results": "查看结果", + "poll_submit": "提交", + "close_poll": "关闭投票", + "results_shared": "结果共享到聊天室。", + "question_required": "您需要输入一个问题!", + "add_answer": "添加答案", + "failed": "创建投票失败,请稍后重试。", + "question_label": "问你的问题*", + "create_poll_menu_option": "创建投票", + "poll_status_closed": "投票结束", + "poll_status_disclosed": "结果将在投票结束时显示。", + "poll_undisclosed": "关闭 - 用户将在投票关闭时看到结果。", + "answer_required": "答案不能为空。 请输入一些文本或删除此选项。", + "num_answered": "{count} 答案" + }, + "export": { + "fetched_n_events": "获取了 {count} 个事件", + "exported_date": "于 {date} 导出", + "fetched_n_of_total_events": "已获取 {count} 个事件,共 {total} 个事件", + "processed_n_of_total_events": "已处理 {count} 个事件的媒体,共 {total} 个事件", + "export_filename": "导出的聊天 {date}" } } diff --git a/src/components/Chat.vue b/src/components/Chat.vue index d242d4f..a551e1f 100644 --- a/src/components/Chat.vue +++ b/src/components/Chat.vue @@ -42,7 +42,7 @@ -
+
@@ -52,7 +52,7 @@ touchStart(e, event); } " v-on:touchend="touchEnd" v-on:touchcancel="touchCancel" v-on:touchmove="touchMove"> - -
+ +
@@ -111,7 +113,7 @@ {{ typingMembersString }} - + import Vue from "vue"; -import { TimelineWindow, EventTimeline, AbortError } from "matrix-js-sdk"; +import { TimelineWindow, EventTimeline } from "matrix-js-sdk"; import util from "../plugins/utils"; import MessageOperations from "./messages/MessageOperations.vue"; import AvatarOperations from "./messages/AvatarOperations.vue"; @@ -280,6 +282,7 @@ import ChatHeader from "./ChatHeader"; import VoiceRecorder from "./VoiceRecorder"; import RoomInfoBottomSheet from "./RoomInfoBottomSheet"; import CreatedRoomWelcomeHeader from "./CreatedRoomWelcomeHeader"; +import NoHistoryRoomWelcomeHeader from "./NoHistoryRoomWelcomeHeader.vue"; import MessageOperationsBottomSheet from "./MessageOperationsBottomSheet"; import StickerPickerBottomSheet from "./StickerPickerBottomSheet"; import BottomSheet from "./BottomSheet.vue"; @@ -329,6 +332,7 @@ export default { VoiceRecorder, RoomInfoBottomSheet, CreatedRoomWelcomeHeader, + NoHistoryRoomWelcomeHeader, MessageOperationsBottomSheet, StickerPickerBottomSheet, BottomSheet, @@ -552,6 +556,34 @@ export default { return util.useVoiceMode(this.room); }, }, + /** + * If we have no events and the room is encrypted, show info about this + * to the user. + */ + showNoHistoryRoomWelcomeHeader() { + return this.filteredEvents.length == 0 && this.room && this.$matrix.matrixClient.isRoomEncrypted(this.room.roomId); + }, + + filteredEvents() { + if (this.room && this.$matrix.matrixClient.isRoomEncrypted(this.room.roomId)) { + if (this.room.getHistoryVisibility() == "joined") { + // For encrypted rooms where history is set to "joined" we can't read old events. + // We might, however, have old status events from room creation etc. + // We filter out anything that happened before our own join event. + for (let idx = this.events.length - 1; idx >= 0; idx--) { + const e = this.events[idx]; + if (e.getType() == "m.room.member" && + e.getContent().membership == "join" && + (!e.getPrevContent() || e.getPrevContent().membership != "join") && + e.getStateKey() == this.$matrix.currentUserId) { + // Our own join event. + return this.events.slice(idx + 1); + } + } + } + } + return this.events; + } }, watch: { @@ -971,12 +1003,22 @@ export default { if (file) { var reader = new FileReader(); reader.onload = (e) => { - this.currentSendShowSendButton = true; + const file = event.target.files[0]; if (file.type.startsWith("image/")) { const currentImageInput = this.optimizeImage(e, event, file) this.currentImageInputs = Array.isArray(this.currentImageInputs) ? [...this.currentImageInputs, currentImageInput] : [currentImageInput] } this.currentImageInputsPath = Array.isArray(this.currentImageInputsPath) ? [...this.currentImageInputsPath, file] : [file]; + console.log(this.currentImageInput); + this.$matrix.matrixClient.getMediaConfig().then((config) => { + this.currentImageInputPath = file; + if (config["m.upload.size"] && file.size > config["m.upload.size"]) { + this.currentSendError = this.$t("message.upload_file_too_large"); + this.currentSendShowSendButton = false; + } else { + this.currentSendShowSendButton = true; + } + }); }; reader.readAsDataURL(file); } @@ -1023,7 +1065,7 @@ export default { } }) .catch((err) => { - if (err instanceof AbortError || err === "Abort") { + if (err.name === "AbortError" || err === "Abort") { this.currentSendError = null; } else { this.currentSendError = err.LocaleString(); diff --git a/src/components/CreateRoom.vue b/src/components/CreateRoom.vue index c2324ad..efc0ec2 100644 --- a/src/components/CreateRoom.vue +++ b/src/components/CreateRoom.vue @@ -43,13 +43,23 @@ v-on:keyup.enter="$refs.create.$el.focus()" :disabled="step > steps.INITIAL" solo> - \ No newline at end of file diff --git a/src/main.js b/src/main.js index 9172559..9e8ee19 100644 --- a/src/main.js +++ b/src/main.js @@ -1,6 +1,5 @@ import Vue from 'vue' import App from './App.vue' -import vuetify from './plugins/vuetify'; import store from './store' import i18n from './plugins/lang'; import router from './router' @@ -15,6 +14,7 @@ import VueResize from 'vue-resize'; import 'vue-resize/dist/vue-resize.css'; import VueClipboard from 'vue-clipboard2' import VueSanitize from "vue-sanitize"; +import createVuetify from './plugins/vuetify'; var defaultOptions = VueSanitize.defaults; defaultOptions.disallowedTagsMode = "recursiveEscape"; @@ -26,11 +26,18 @@ Vue.config.productionTip = false Vue.use(VueResize); Vue.use(VEmojiPicker); Vue.use(matrix, { store: store, i18n: i18n }); -// eslint-disable-next-line -Vue.use(config, globalThis.window.location.origin); // Use this before cleaninsights below, it depends on config! + +const configLoadedPromise = new Promise((resolve, ignoredreject) => { + // eslint-disable-next-line + Vue.use(config, globalThis.window.location.origin, (config) => { + resolve(config); + }); // Use this before cleaninsights below, it depends on config! +}); Vue.use(analytics); Vue.use(VueClipboard); +const vuetify = createVuetify(config); + // Add bubble functionality to custom events. // From here: https://stackoverflow.com/questions/41993508/vuejs-bubbling-custom-events Vue.use((Vue) => { @@ -161,7 +168,7 @@ Vue.directive('longTap', { Vue.use(navigation, router); -new Vue({ +const vueInstance = new Vue({ vuetify, store, i18n, @@ -170,4 +177,10 @@ new Vue({ config, analytics, render: h => h(App) -}).$mount('#app'); \ No newline at end of file +}); +vueInstance.$vuetify.theme.themes.light.primary = vueInstance.$config.accentColor; +configLoadedPromise.then((config) => { + vueInstance.$vuetify.theme.themes.light.primary = config.accentColor; + vueInstance.$mount('#app'); +}); + diff --git a/src/plugins/utils.js b/src/plugins/utils.js index 8d0eca5..873b126 100644 --- a/src/plugins/utils.js +++ b/src/plugins/utils.js @@ -28,11 +28,12 @@ var _browserCanRecordAudioF = function () { } var _browserCanRecordAudio = _browserCanRecordAudioF(); -class AbortablePromise extends Promise { +class UploadPromise extends Promise { constructor(executor) { const aborter = { aborted: false, abortablePromise: undefined, + matrixClient: undefined, } const normalExecutor = function (resolve, reject) { @@ -42,8 +43,9 @@ class AbortablePromise extends Promise { super(normalExecutor); this.abort = () => { aborter.aborted = true; - if (aborter.abortablePromise) { - aborter.abortablePromise.abort(); + if (aborter.abortablePromise && aborter.matrixClient) { + aborter.matrixClient.cancelUpload(aborter.abortablePromise); + aborter.matrixClient = undefined; aborter.abortablePromise = undefined; } }; @@ -320,7 +322,7 @@ class Util { } sendImage(matrixClient, roomId, file, onUploadProgress) { - return new AbortablePromise((resolve, reject, aborter) => { + return new UploadPromise((resolve, reject, aborter) => { const abortionController = aborter; var reader = new FileReader(); reader.onload = (e) => { @@ -366,6 +368,7 @@ class Util { if (!matrixClient.isRoomEncrypted(roomId)) { // Not encrypted. + abortionController.matrixClient = matrixClient; abortionController.abortablePromise = matrixClient.uploadContent(data, opts); abortionController.abortablePromise .then((response) => { diff --git a/src/plugins/vuetify.js b/src/plugins/vuetify.js index 2f802b3..dc74d40 100644 --- a/src/plugins/vuetify.js +++ b/src/plugins/vuetify.js @@ -14,12 +14,22 @@ function importAll(r) { } importAll(require.context('@/assets/icons/', true, /\.vue$/)); - Vue.use(Vuetify); -export default new Vuetify({ - icons: { - iconfont: 'md', - values: icons, - }, -}); +export default function(ignoredconfig) { + return new Vuetify({ + icons: { + iconfont: 'md', + values: icons, + }, + options: { + customProperties: true + }, + theme: { + options: { + customProperties: true, + }, + dark: false, + } + }); +} diff --git a/src/services/config.service.js b/src/services/config.service.js index 7227a38..af46cc1 100644 --- a/src/services/config.service.js +++ b/src/services/config.service.js @@ -1,13 +1,14 @@ export default { - install(Vue, defaultServerFromLocation) { + install(Vue, defaultServerFromLocation, onloaded) { var config = Vue.observable(require('@/assets/config.json')); - const getRuntimeConfig = async () => { - const runtimeConfig = await fetch('./config.json'); - return await runtimeConfig.json() + const getRuntimeConfig = () => { + return fetch('./config.json').then((res) => res.json()).catch(err => { + console.error("Failed to get config:", err); + return {}; + }); } - config.promise = getRuntimeConfig(); - config.promise.then(function (json) { + config.promise = getRuntimeConfig().then((json) => { // Reactively use all the config values for (const key of Object.keys(json)) { Vue.set(config, key, json[key]); @@ -16,6 +17,12 @@ export default { if (!json.defaultServer) { Vue.set(config, "defaultServer", defaultServerFromLocation); } + + // Tell callback we are done loading runtime config + if (onloaded) { + onloaded(config); + } + return config; }); Vue.prototype.$config = config; } diff --git a/src/services/matrix.service.js b/src/services/matrix.service.js index 1013b07..658f9bf 100644 --- a/src/services/matrix.service.js +++ b/src/services/matrix.service.js @@ -94,9 +94,18 @@ export default { immediate: true, handler(roomId) { this.currentRoom = this.getRoom(roomId); - this.currentRoomIsReadOnlyForUser = this.isReadOnlyRoomForUser(roomId, this.currentUserId); }, }, + currentRoom: { + immediate: true, + handler(room) { + if (room) { + this.currentRoomIsReadOnlyForUser = this.isReadOnlyRoomForUser(room.roomId, this.currentUserId); + } else { + this.currentRoomIsReadOnlyForUser = false; + } + } + } }, methods: { @@ -863,7 +872,17 @@ export default { type: "m.room.history_visibility", state_key: "", content: { - history_visibility: "joined", + history_visibility: "invited", + }, + }, + { + type: "m.room.power_levels", + state_key: "", + content: { + users: { + [this.currentUserId]: 100, + [userId]: 100, + }, }, }, ], @@ -872,7 +891,6 @@ export default { return this.matrixClient .createRoom(createRoomOptions) .then(({ room_id, room_alias }) => { - this.makeAdmin(room_id, userId); // Make the other user an equal resolve(this.getRoom(room_alias || room_id)); }) .catch((error) => {