merge master

This commit is contained in:
10G Meow 2023-05-18 13:48:31 +03:00
commit 8ca2d8ec07
28 changed files with 608 additions and 95 deletions

View file

@ -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 <path-to-sticker-packs>"
* 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)

View file

@ -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;

View file

@ -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"
}

View file

@ -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 {

View file

@ -0,0 +1,7 @@
<template>
<svg width="29" height="29" viewBox="0 0 29 29" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M25.619 7.08953L25.5625 7.07888C21.9335 6.38303 18.5747 4.43518 15.2936 1.12288L15.279 1.10825C15.079 0.906348 14.8083 0.790138 14.5241 0.784177C14.2399 0.778215 13.9646 0.88297 13.7563 1.0763L13.7064 1.12288C10.4253 4.43518 7.06646 6.38303 3.4375 7.07889L3.38096 7.08954C3.13088 7.13759 2.90535 7.27127 2.74316 7.46759C2.58096 7.6639 2.49221 7.91059 2.49219 8.16524V8.22179C2.49207 12.2448 3.55097 16.197 5.56245 19.681C7.57393 23.165 10.4671 26.0582 13.9512 28.0696C14.1095 28.1608 14.2881 28.211 14.4707 28.2156C14.6533 28.2203 14.8342 28.1792 14.9969 28.0962L15.0488 28.0696C18.5329 26.0582 21.4261 23.165 23.4375 19.681C25.449 16.197 26.5079 12.2448 26.5078 8.22179V8.16524C26.5078 7.91059 26.419 7.6639 26.2568 7.46758C26.0946 7.27126 25.8691 7.13759 25.619 7.08953ZM14.5 25.6827V3.59895C17.5695 6.48016 20.7467 8.30761 24.1581 9.15314C24.0015 12.4832 23.0412 15.7258 21.3594 18.6042C19.6776 21.4826 17.3241 23.9112 14.5 25.6827Z"
fill="#1D1D1D" />
</svg>
</template>

View file

@ -0,0 +1,7 @@
<template>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M1.35799 14.8085L8.18319 2.98652C8.55719 2.33892 9.25399 1.93652 10.002 1.93652C10.75 1.93652 11.4468 2.33892 11.8208 2.98652L18.7066 14.9137C19.0804 15.5613 19.0804 16.3659 18.7066 17.0137C18.3326 17.6613 17.6358 18.0637 16.8878 18.0637H3.11199C1.95399 18.0637 1.01199 17.1217 1.01199 15.9637C1.01219 15.5505 1.13159 15.1517 1.35799 14.8085ZM3.11219 16.6635H16.8878C17.137 16.6635 17.3694 16.5293 17.494 16.3135C17.6186 16.0975 17.6186 15.8293 17.494 15.6135L10.608 3.68672C10.4834 3.47072 10.251 3.33672 10.0018 3.33672C9.75259 3.33672 9.52019 3.47092 9.39559 3.68672L2.55879 15.5283C2.55039 15.5429 2.54139 15.5573 2.53179 15.5713C2.45319 15.6869 2.41179 15.8227 2.41179 15.9637C2.41219 16.3495 2.72619 16.6635 3.11219 16.6635ZM9.99999 12.7861C9.56799 12.7861 9.21759 12.4359 9.21759 12.0037V7.93752C9.21759 7.50532 9.56799 7.15512 9.99999 7.15512C10.432 7.15512 10.7824 7.50532 10.7824 7.93752V12.0037C10.7824 12.4359 10.432 12.7861 9.99999 12.7861ZM9.99999 13.2451C10.264 13.2451 10.5202 13.3511 10.708 13.5371C10.894 13.7231 11 13.9811 11 14.2451C11 14.5071 10.894 14.7651 10.708 14.9511C10.5202 15.1371 10.2622 15.2451 9.99999 15.2451C9.73799 15.2451 9.47999 15.1371 9.29399 14.9511C9.10619 14.7651 8.99999 14.5071 8.99999 14.2451C8.99999 13.9811 9.10599 13.7231 9.29399 13.5371C9.47999 13.3509 9.73799 13.2451 9.99999 13.2451Z"
fill="#1D1D1D" />
</svg>
</template>

Binary file not shown.

View file

@ -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": "དྲིས་ལན།",

View file

@ -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",

View file

@ -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",

View file

@ -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}"
}
}

View file

@ -42,7 +42,7 @@
<CreatedRoomWelcomeHeader v-if="showCreatedRoomWelcomeHeader" v-on:close="closeCreateRoomWelcomeHeader" />
<div v-for="(event, index) in events" :key="event.getId()" :eventId="event.getId()">
<div v-for="(event, index) in filteredEvents" :key="event.getId()" :eventId="event.getId()">
<!-- DAY Marker, shown for every new day in the timeline -->
<div v-if="showDayMarkerBeforeEvent(event) && !!componentForEvent(event, isForExport = false)" class="day-marker" :title="dayForEvent(event)" />
@ -52,7 +52,7 @@
touchStart(e, event);
}
" v-on:touchend="touchEnd" v-on:touchcancel="touchCancel" v-on:touchmove="touchMove">
<component :is="componentForEvent(event)" :room="room" :originalEvent="event" :nextEvent="events[index + 1]"
<component :is="componentForEvent(event)" :room="room" :originalEvent="event" :nextEvent="filteredEvents[index + 1]"
:timelineSet="timelineSet" v-on:send-quick-reaction.stop="sendQuickReaction"
v-on:context-menu="showContextMenuForEvent($event)" v-on:own-avatar-clicked="viewProfile"
v-on:other-avatar-clicked="showAvatarMenuForEvent($event)" v-on:download="download(event)"
@ -64,11 +64,13 @@
/>
<!-- <div v-if="debugging" style="user-select:text">EventID: {{ event.getId() }}</div> -->
<!-- <div v-if="debugging" style="user-select:text">Event: {{ JSON.stringify(event) }}</div> -->
<div v-if="event.getId() == readMarker && index < events.length - 1" class="read-marker"
<div v-if="event.getId() == readMarker && index < filteredEvents.length - 1" class="read-marker"
:title="$t('message.unread_messages')" />
</div>
</div>
</div>
<NoHistoryRoomWelcomeHeader v-if="showNoHistoryRoomWelcomeHeader" />
</div>
<!-- Input area -->
@ -111,7 +113,7 @@
{{ typingMembersString }}
</div>
</v-row>
<v-row class="input-area-inner align-center" v-if="!showRecorder">
<v-row class="input-area-inner align-center" v-if="!showRecorder && !$matrix.currentRoomIsReadOnlyForUser">
<v-col class="flex-grow-1 flex-shrink-1 ma-0 pa-0">
<v-textarea height="undefined" ref="messageInput" full-width auto-grow rows="1" v-model="currentInput"
no-resize class="input-area-text" :placeholder="$t('message.your_message')" hide-details
@ -272,7 +274,7 @@
<script>
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();

View file

@ -43,13 +43,23 @@
v-on:keyup.enter="$refs.create.$el.focus()" :disabled="step > steps.INITIAL" solo></v-text-field>
<!-- Our only option right now is voice mode, so if not enabled, hide the 'options' drop down as well -->
<template v-if="$config.experimental_voice_mode || $config.experimental_read_only_room">
<template v-if="$config.experimental_voice_mode || $config.experimental_read_only_room || $config.experimental_public_room">
<div @click.stop="showOptions = !showOptions" v-show="roomName.length > 0" class="options clickable">
<div>{{ $t("new_room.options") }}</div>
<v-icon v-if="!showOptions">expand_more</v-icon>
<v-icon v-else>expand_less</v-icon>
</div>
<v-card v-if="$config.experimental_voice_mode" v-show="showOptions" class="account ma-3" flat>
<v-card v-if="$config.experimental_public_room" v-show="showOptions" class="room-option account ma-0" flat>
<v-card-text class="with-right-label">
<div>
<div class="option-title">{{ $t('room_info.make_public') }}</div>
<!-- <div class="option-text">{{ $t('room_info.read_only_room_info') }}</div> -->
</div>
<v-switch v-model="unencryptedRoom"></v-switch>
</v-card-text>
<div class="option-warning" v-if="unencryptedRoom"><v-icon size="18">$vuetify.icons.ic_warning</v-icon>{{ $t("room_info.make_public_warning")}}</div>
</v-card>
<v-card v-if="$config.experimental_voice_mode" v-show="showOptions" class="room-option account ma-0" flat>
<v-card-text class="with-right-label">
<div>
<div class="option-title">{{ $t('room_info.voice_mode') }}</div>
@ -58,7 +68,7 @@
<v-switch v-model="useVoiceMode"></v-switch>
</v-card-text>
</v-card>
<v-card v-if="$config.experimental_read_only_room" v-show="showOptions" class="account ma-3" flat>
<v-card v-if="$config.experimental_read_only_room" v-show="showOptions" class="room-option account ma-0" flat>
<v-card-text class="with-right-label">
<div>
<div class="option-title">{{ $t('room_info.read_only_room') }}</div>
@ -186,6 +196,7 @@ export default {
roomNameHasError: false,
roomCreationErrorMsg: "",
showOptions: false,
unencryptedRoom: false,
useVoiceMode: false,
readOnlyRoom: false,
};
@ -316,7 +327,17 @@ export default {
visibility: "private", // Not listed!
name: this.roomName,
preset: "public_chat",
initial_state: [
initial_state:
this.unencryptedRoom ? [
{
type: "m.room.history_visibility",
state_key: "",
content: {
history_visibility: "shared"
}
}
] :
[
{
type: "m.room.encryption",
state_key: "",

View file

@ -4,7 +4,7 @@
<v-container fluid class="text-center mt-8">
<v-row align="center" justify="center">
<v-col class="text-center" cols="auto">
<v-img contain src="@/assets/logo.svg" width="64" height="64" />
<v-img contain :src="logotype" width="64" height="64" />
</v-col>
</v-row>
</v-container>
@ -42,12 +42,13 @@
<script>
import RoomList from "../components/RoomList";
import YouAre from "../components/YouAre.vue";
import logoMixin from "../components/logoMixin";
export default {
components: {
RoomList,
YouAre,
},
mixins: [logoMixin],
computed: {
loading() {
return !this.$matrix.ready;

View file

@ -123,7 +123,7 @@
<div class="d-flex justify-center align-center mt-9">
<div class="mr-2">
<img src="@/assets/logo.svg" width="32" height="32" contain class="d-inline" />
<img :src="logotype" width="32" height="32" contain class="d-inline" />
</div>
<div>
<strong>{{ $t("project.name") }}</strong>
@ -138,12 +138,12 @@ import util from "../plugins/utils";
import InteractiveAuth from './InteractiveAuth.vue';
import LanguageMixin from "./languageMixin";
import rememberMeMixin from "./rememberMeMixin";
import logoMixin from "./logoMixin";
import SelectLanguageDialog from "./SelectLanguageDialog.vue";
export default {
name: "Join",
mixins: [LanguageMixin, rememberMeMixin],
mixins: [LanguageMixin, rememberMeMixin, logoMixin],
components: {
SelectLanguageDialog,
InteractiveAuth

View file

@ -5,7 +5,7 @@
<v-row no-gutters>
<v-col>
<v-img
src="@/assets/logo.svg"
:src="logotype"
width="32"
height="32"
contain
@ -106,10 +106,11 @@ import User from "../models/user";
import util from "../plugins/utils";
import rememberMeMixin from "./rememberMeMixin";
import * as sdk from "matrix-js-sdk";
import logoMixin from "./logoMixin";
export default {
name: "Login",
mixins:[rememberMeMixin],
mixins:[rememberMeMixin, logoMixin],
data() {
return {
user: new User(this.$config.defaultServer, "", ""),

View file

@ -0,0 +1,19 @@
<template>
<div class="text-center">
<v-icon size="27" class="shield">$vuetify.icons.ic_security-shield</v-icon>
<div>{{ $t("room_welcome.no_past_messages") }}</div>
</div>
</template>
<script>
export default {
name: "NoHistoryRoomWelcomeHeader",
};
</script>
<style lang="scss">
@import "@/assets/css/chat.scss";
.shield {
margin-bottom: 12px;
}
</style>

View file

@ -2,10 +2,10 @@
<transition name="grow" mode="out-in">
<div
v-show="show"
:class="{ 'voice-recorder': true, ptt: ptt, row: !ptt }"
:class="{ 'voice-recorder': true, ptt: usePTT, row: !usePTT }"
ref="vrroot"
>
<v-container v-if="!ptt" fluid fill-height>
<v-container v-if="!usePTT" fluid fill-height>
<v-row align="center" class="mt-3">
<v-col cols="4" align="center">
<v-btn v-show="state == states.RECORDED" icon @click.stop="redo">
@ -71,7 +71,7 @@
{{ recordingTime }}
</div>
</v-col>
<v-col cols="6" v-if="ptt">
<v-col cols="6" v-if="usePTT">
<div class="swipe-info">
&lt;&lt; {{ $t("voice_recorder.swipe_to_cancel") }}
</div>
@ -146,7 +146,7 @@
</div>
<VoiceRecorderLock
v-show="state == states.RECORDING && ptt"
v-show="state == states.RECORDING && usePTT"
:style="lockButtonStyle"
:isLocked="recordingLocked"
/>
@ -209,6 +209,9 @@ export default {
errorMessage: null,
recorder: null,
previewPlayer: null,
wakeLock: null,
maxRecordingLength: 300, // In seconds
forceNonPTTMode: false,
};
},
watch: {
@ -244,13 +247,14 @@ export default {
}
},
show(val) {
this.forceNonPTTMode = false;
if (val) {
// Add listeners
this.state = State.INITIAL;
this.errorMessage = null;
this.recordedFile = null;
this.recordingTime = String.fromCharCode(160);
if (this.ptt) {
if (this.usePTT) {
document.addEventListener("mouseup", this.mouseUp, false);
document.addEventListener("mousemove", this.mouseMove, false);
document.addEventListener("touchend", this.mouseUp, false);
@ -288,6 +292,9 @@ export default {
}
},
computed: {
usePTT() {
return this.ptt && !this.forceNonPTTMode;
},
lockButtonStyle() {
/**
Calculate where to show the lock button (it should be at the same X-coord as the)
@ -366,6 +373,9 @@ export default {
this.recordStartedAt = Date.now();
this.startRecordTimer();
})
.then(async () => {
this.aquireWakeLock();
})
.catch((e) => {
console.error(e);
if (e && e.name == "NotAllowedError") {
@ -374,25 +384,65 @@ export default {
this.state = State.ERROR;
});
},
screenLocked() {
if (document.visibilityState === "hidden" && this.state == State.RECORDING) {
this.pauseRecording();
}
},
playRecordedSound() {
const audio = new Audio(require("@/assets/sounds/record_stop.mp3"));
audio.play();
},
aquireWakeLock() {
document.addEventListener("visibilitychange", this.screenLocked);
try {
if (navigator.wakeLock && !this.wakeLock) {
navigator.wakeLock.request('screen').then((lock) => this.wakeLock = lock);
}
}
catch(err) { console.error(err)}
},
releaseWakeLock() {
document.removeEventListener("visibilitychange", this.screenLocked);
if (this.wakeLock) {
this.wakeLock.release().then(() => {
this.wakeLock = null;
});
}
},
cancelRecording() {
if(this.recorder) {
this.recorder.stop();
this.recorder = null;
}
this.releaseWakeLock();
this.state = State.INITIAL;
this.close();
},
pauseRecording() {
// Remove PTT mode. We can get here in PTT if screen is locked or if max time is reached.
if (this.ptt) {
this.forceNonPTTMode = true;
this.recordingLocked = false;
document.removeEventListener("mouseup", this.mouseUp, false);
document.removeEventListener("mousemove", this.mouseMove, false);
document.removeEventListener("touchend", this.mouseUp, false);
document.removeEventListener("touchmove", this.mouseMove, false);
}
this.state = State.RECORDED;
this.stopRecordTimer();
this.releaseWakeLock();
this.getFile(false);
this.playRecordedSound();
},
stopRecording() {
this.state = State.RECORDED;
this.stopRecordTimer();
this.releaseWakeLock();
this.recordingTime = String.fromCharCode(160); // nbsp;
this.close();
this.getFile(true);
this.playRecordedSound();
},
redo() {
this.state = State.INITIAL;
@ -431,6 +481,10 @@ export default {
this.recordingTime = util.formatRecordDuration(
now - this.recordStartedAt
);
// Auto-stop?
if ((now - this.recordStartedAt) >= (1000 * this.maxRecordingLength) && this.state == State.RECORDING) {
this.pauseRecording();
}
}, 500);
},
stopRecordTimer() {

View file

@ -94,6 +94,15 @@ export default {
CreatePollDialog,
},
methods: {
showOnlyUserStatusMessages() {
// We say that if you can redact events, you are allowed to create polls.
// NOTE!!! This assumes that there is a property named "room" on THIS.
const me = this.room && this.room.getMember(this.$matrix.currentUserId);
let isModerator =
me && this.room.currentState && this.room.currentState.hasSufficientPowerLevelFor("redact", me.powerLevel);
const show = this.$config.show_status_messages;
return show === "never" || (show === "moderators" && !isModerator)
},
showDayMarkerBeforeEvent(event) {
const idx = this.events.indexOf(event);
if (idx <= 0) {
@ -132,10 +141,12 @@ export default {
return ContactKicked;
}
return ContactLeave;
} else if (event.getContent().membership == "invite") {
return ContactInvited;
} else if (event.getContent().membership == "ban") {
return ContactBanned;
} else if (!this.showOnlyUserStatusMessages()) {
if (event.getContent().membership == "invite") {
return ContactInvited;
} else if (event.getContent().membership == "ban") {
return ContactBanned;
}
}
break;
@ -203,34 +214,64 @@ export default {
}
case "m.room.create":
return RoomCreated;
if (!this.showOnlyUserStatusMessages()) {
return RoomCreated;
}
break;
case "m.room.canonical_alias":
return RoomAliased;
if (!this.showOnlyUserStatusMessages()) {
return RoomAliased;
}
break;
case "m.room.name":
return RoomNameChanged;
if (!this.showOnlyUserStatusMessages()) {
return RoomNameChanged;
}
break;
case "m.room.topic":
return RoomTopicChanged;
if (!this.showOnlyUserStatusMessages()) {
return RoomTopicChanged;
}
break;
case "m.room.avatar":
return RoomAvatarChanged;
if (!this.showOnlyUserStatusMessages()) {
return RoomAvatarChanged;
}
break;
case "m.room.history_visibility":
return RoomHistoryVisibility;
if (!this.showOnlyUserStatusMessages()) {
return RoomHistoryVisibility;
}
break;
case "m.room.join_rules":
return RoomJoinRules;
if (!this.showOnlyUserStatusMessages()) {
return RoomJoinRules;
}
break;
case "m.room.power_levels":
return RoomPowerLevelsChanged;
if (!this.showOnlyUserStatusMessages()) {
return RoomPowerLevelsChanged;
}
break;
case "m.room.guest_access":
return RoomGuestAccessChanged;
if (!this.showOnlyUserStatusMessages()) {
return RoomGuestAccessChanged;
}
break;
case "m.room.encryption":
return RoomEncrypted;
if (!this.showOnlyUserStatusMessages()) {
return RoomEncrypted;
}
break;
case "m.poll.start":
case "org.matrix.msc3381.poll.start":

View file

@ -0,0 +1,10 @@
export default {
computed: {
logotype() {
if (this.$config.logo) {
return this.$config.logo;
}
return require("@/assets/logo.svg");
}
}
}

View file

@ -15,20 +15,23 @@
</v-avatar>
<!-- SLOT FOR CONTENT -->
<slot></slot>
<div class="op-button" ref="opbutton" v-if="!event.isRedacted()">
<div class="op-button" ref="opbutton" v-if="!event.isRedacted() && !$matrix.currentRoomIsReadOnlyForUser">
<v-btn id="btn-more" icon @click.stop="showContextMenu($refs.opbutton)">
<v-icon>more_vert</v-icon>
</v-btn>
</div>
<QuickReactions :event="event" :timelineSet="timelineSet" v-on="$listeners"/>
<SeenBy :room="room" :event="event"/>
</div>
</template>
<script>
import SeenBy from "./SeenBy.vue";
import messageMixin from "./messageMixin";
export default {
mixins: [messageMixin],
mixins: [messageMixin],
components: { SeenBy }
};
</script>

View file

@ -8,7 +8,7 @@
<div class="status">{{ event.status }}</div>
</div>
<div class="op-button" ref="opbutton" v-if="!event.isRedacted()">
<div class="op-button" ref="opbutton" v-if="!event.isRedacted() && !$matrix.currentRoomIsReadOnlyForUser">
<v-btn id="btn-show-menu" icon @click.stop="showContextMenu($refs.opbutton)">
<v-icon>more_vert</v-icon>
</v-btn>
@ -25,14 +25,17 @@
<span v-else class="white--text headline">{{ userAvatarLetter }}</span>
</v-avatar>
<QuickReactions :event="event" :timelineSet="timelineSet" v-on="$listeners"/>
<SeenBy :room="room" :event="event"/>
</div>
</template>
<script>
import SeenBy from "./SeenBy.vue";
import messageMixin from "./messageMixin";
export default {
mixins: [messageMixin],
components: { SeenBy }
};
</script>
<style lang="scss">

View file

@ -0,0 +1,96 @@
<template>
<div class="seen-by-container">
<v-tooltip top open-delay="500" v-if="seenBy.length > 0">
<template v-slot:activator="{ on, attrs }">
<div v-bind="attrs" v-on="on" class="clickable">
<div class="more" v-if="seenBy.length > 0">{{ moreItems }}</div>
<transition-group name="list" tag="div" v-if="seenBy.length > 0">
<v-avatar v-for="(member, index) in seenBy" :key="member.userId" class="seen-by-user" size="16" color="grey"
v-show="index < SHOW_LIMIT">
<img v-if="memberAvatar(member)" :src="memberAvatar(member)" />
<span v-else class="white--text headline">{{
member.name.substring(0, 1).toUpperCase()
}}</span>
</v-avatar>
</transition-group>
</div>
</template>
<span>{{ $tc("message.seen_by", seenBy.length) }}</span>
</v-tooltip>
</div>
</template>
<script>
export default {
props: {
room: {
type: Object,
default: function () {
return null;
},
},
event: {
type: Object,
default: function () {
return null;
}
},
},
data() {
return {
seenBy: [],
SHOW_LIMIT: 5,
}
},
mounted() {
this.update();
if (this.room) {
this.room.on("Room.receipt", this.onReceipt);
}
},
beforeDestroy() {
if (this.room) {
this.room.off("Room.receipt", this.onReceipt);
}
},
computed: {
moreItems() {
if (this.seenBy.length > this.SHOW_LIMIT) {
return `+${this.seenBy.length - this.SHOW_LIMIT}`;
}
return "";
}
},
methods: {
onReceipt(ignoredevent) {
this.update();
},
memberAvatar(member) {
if (member) {
return member.getAvatarUrl(
this.$matrix.matrixClient.getHomeserverUrl(),
16,
16,
"scale",
true
);
}
return null;
},
update() {
this.seenBy = ((this.room && this.event) ? this.room.getReceiptsForEvent(this.event) : [])
.filter(receipt => receipt.type == 'm.read' && receipt.userId !== this.$matrix.currentUserId)
.map(receipt => this.room.getMember(receipt.userId));
},
},
watch: {
event() {
this.update();
}
}
};
</script>
<style lang="scss">
@import "@/assets/css/chat.scss";
</style>

View file

@ -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');
});
vueInstance.$vuetify.theme.themes.light.primary = vueInstance.$config.accentColor;
configLoadedPromise.then((config) => {
vueInstance.$vuetify.theme.themes.light.primary = config.accentColor;
vueInstance.$mount('#app');
});

View file

@ -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) => {

View file

@ -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,
}
});
}

View file

@ -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;
}

View file

@ -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) => {