merge master and improvements

This commit is contained in:
10G Meow 2024-03-09 11:15:48 +02:00
commit 7444891b6d
21 changed files with 748 additions and 208 deletions

View file

@ -7,15 +7,15 @@
<link rel="icon" id="favicon" href="<%= BASE_URL %>favicon.ico" />
<title><%= htmlWebpackPlugin.options.title %></title>
<meta name="apple-mobile-web-app-capable" content="yes" />
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-72x72.png" sizes="72x72" />
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-96x96.png" sizes="96x96" />
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-128x128.png" sizes="128x128" />
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-144x144.png" sizes="144x144" />
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-152x152.png" sizes="152x152" />
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-192x192.png" sizes="192x192" />
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-384x384.png" sizes="384x384" />
<link rel="apple-touch-icon" href="<%= BASE_URL %>icons/icon-512x512.png" sizes="512x512" />
<link rel="manifest" href="<%= BASE_URL %>manifest.json" />
<link rel="apple-touch-icon" href="./icons/icon-72x72.png" sizes="72x72" />
<link rel="apple-touch-icon" href="./icons/icon-96x96.png" sizes="96x96" />
<link rel="apple-touch-icon" href="./icons/icon-128x128.png" sizes="128x128" />
<link rel="apple-touch-icon" href="./icons/icon-144x144.png" sizes="144x144" />
<link rel="apple-touch-icon" href="./icons/icon-152x152.png" sizes="152x152" />
<link rel="apple-touch-icon" href="./icons/icon-192x192.png" sizes="192x192" />
<link rel="apple-touch-icon" href="./icons/icon-384x384.png" sizes="384x384" />
<link rel="apple-touch-icon" href="./icons/icon-512x512.png" sizes="512x512" />
<link rel="manifest" href="./manifest.json" />
<script src="./lottie-player.js"></script>
<style>
#loader {

View file

@ -1,6 +1,5 @@
{
"id": "/",
"start_url": "/",
"start_url": ".",
"name": "Convene - Chat for everyone ",
"short_name": "Convene",
"theme_color": "#FFFFFF",
@ -8,42 +7,42 @@
"display": "standalone",
"icons": [
{
"src": "/icons/icon-72x72.png",
"src": "./icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "/icons/icon-96x96.png",
"src": "./icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "/icons/icon-128x128.png",
"src": "./icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "/icons/icon-144x144.png",
"src": "./icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "/icons/icon-152x152.png",
"src": "./icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "/icons/icon-192x192.png",
"src": "./icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-384x384.png",
"src": "./icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "/icons/icon-512x512.png",
"src": "./icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}

View file

@ -1,8 +1,6 @@
// Notification click event listener
self.addEventListener("notificationclick", (e) => {
// Close the notification popout
e.notification.close();
// Get all the Window clients
e.waitUntil(
clients.matchAll({ type: "window" }).then((clientsArr) => {
// If a Window tab matching the targeted URL already exists, focus that;
@ -26,9 +24,10 @@ async function checkNewMessages() {
// Todo...
}
// Install PWA in mobile or web to test if periodicSync notification works
// see browser compatibility: https://developer.mozilla.org/en-US/docs/Web/API/Web_Periodic_Background_Synchronization_API#browser_compatibility
self.addEventListener('periodicsync', (event) => {
if (event.tag === 'check-new-messages') {
// Test if periodicSync notification triggers in Mobile app(created via add to homescreen)
self.registration.showNotification("Notification via periodicSync");
event.waitUntil(checkNewMessages());

View file

@ -994,21 +994,12 @@ body {
margin-left: 38px;
}
.member::after {
content: " ";
display: block;
margin: 10px 0px;
bottom: 0px;
height: 1px;
background-color: #e1e1e1;
width: 100%;
}
.show-all {
color: black;
font-size: 14 * $chat-text-size;
font-weight: bold;
margin-left: 10px;
padding: 10px;
[dir="rtl"] & {
margin-left: initial;
margin-right: 10px;

View file

@ -34,7 +34,16 @@
"voice_mode": "སྐད་སྒྲའི་རྣམ་པ།",
"voice_mode_info": "ཁ་བརྡའི་འབྲེལ་མཐུད་དེ་ཉན་པ་དང་སྒྲ་ཕབ་ཀྱི་རྣམ་པའི་ནང་དུ་བསྒྱུར།",
"download_chat": "ཁ་བརྡ་ཕབ་ལེན།",
"read_only_room": "ཁ་བརྡ་ཁང་དུ་ཀློག་མ་གཏོགས་མི་ཆོག"
"read_only_room": "ཁ་བརྡ་ཁང་དུ་ཀློག་མ་གཏོགས་མི་ཆོག",
"direct_link": "ང་དང་ཐད་ཀར་ཁ་བརྡ་བྱེད་སའི་འབྲེལ་ཐག",
"make_public_warning": "ཉེན་བརྡ།: ཆ་འཕྲིན་གྱི་ལོ་རྒྱུས་ཡོངས་རྫོགས་འཛུལ་ཞུགས་པ་གསར་པ་ཚོས་མཐོང་ཐུབ།",
"room_type": "ཁ་བརྡ་ཁང་གི་དབྱེ་བ།",
"file_mode_info": "གླེང་མོལ་གྱི་མཐུད་ངོས་ཡིག་ཆ་བརྒྱུད་གཏོང་གི་རྣམ་པར་བསྒྱུར།",
"room_type_default": "སྔར་ཡོད།",
"file_mode": "ཡིག་ཆའི་རྣམ་པ།",
"direct_link_desc": "བརྒྱུད་སྐུར་བྱེད་པར་གྲ་སྒྲིག་ཡིན། མི་ཞིག་གིས་འབྲེལ་ཐག་དེ་ཁ་འབྱེད་ཐེངས་རེར། ང་དང་ཐད་ཀར་གླེང་མོལ་བྱ་སའི་ཁ་བརྡ་ཁང་ཞིག་གི་སྒོ་འབྱེད་ངེས།",
"copy_link": "འབྲེལ་ཐག་པར་བཤུས་རྒྱོབས།",
"make_public": "ཡོངས་ཁྱབ་བཟོས།"
},
"invite": {
"done": "ལེགས་འགྲུབ།",
@ -60,7 +69,10 @@
"send_verification": "ར་སྤྲོད་ཡིག་ཟམ་ཐོངས།",
"no_supported_flow": "བཀོལ་ཆས་འདི་སྤྲད་ཡོད་པའི་དྲ་བའི་ཞབས་ཞུ་ཆས་ནང་དུ་འཛུལ་ཐུབ་ཀྱི་མི་འདུག",
"sent_verification": "{email}ཡིག་ཟམ་ཁ་བྱང་འདིའི་ཐོག་ཏུ་འཕྲིན་པ་ཞིག་བཏང་ཡོད། ཡིག་ཟམ་ནང་འཛུལ་ཏེ་སོ་སོའི་ཡིག་ཟམ་ཁ་བྱང་ར་སྤྲོད་བྱེད་རོགས།",
"email_not_valid": "ཡིག་ཟམ་ཁ་བྱང་བེད་མེད་རེད་འདུག"
"email_not_valid": "ཡིག་ཟམ་ཁ་བྱང་བེད་མེད་རེད་འདུག",
"registration_token": "ཐོ་འགོད་མཚོན་རྟགས་གཏགས་རོགས།",
"token_not_valid": "མཚོན་རྟགས་རྩིས་མེད།",
"send_token": "མཚོན་རྟགས་ཐོངས།"
},
"new_room": {
"next": "རྗེས་མ།",
@ -140,7 +152,10 @@
"got_it": "ཧ་གོ་སོང་།",
"room_history_joined": "ཚོགས་མི་ཁག་ཁ་བརྡ་ཁང་དུ་ཞུགས་པའི་རྗེས་སུ། ད་གཟོད་དེའི་ནང་དུ་བཏང་ཡོད་པའི་འཕྲིན་ཐུང་ཁག་མཐོང་ཐུབ།",
"room_history_is": "ཁ་བརྡ་ཁང་གི་ཟིན་ཐོ་ཁག {type}.",
"encrypted": "འཕྲིན་ཐུང་ཁག་ལ་སྣེ་གཉིས་བར་གྱི་གསང་སྡོམ་བྱས་ཡོད།"
"encrypted": "འཕྲིན་ཐུང་ཁག་ལ་སྣེ་གཉིས་བར་གྱི་གསང་སྡོམ་བྱས་ཡོད།",
"no_past_messages": "དགའ་བསུ་ཞུ། ཁྱེད་ཀྱི་བདེ་འཇགས་ཀྱི་ཆེད་དུ། སྔོན་གྱི་ཆ་འཕྲིན་ཁག་ལྟ་ཀློག་མི་ཐུབ།",
"direct_info": "བཀྲ་ཤིས་བདེ་ལེགས། {you} ཁྱེད་རང་{user}་དང་སྒེར་གྱི་ཁ་བརྡ་བྱེད་བཞིན་ཡོད།",
"direct_private_chat": "ཐད་ཀའི་འཕྲིན་ཐུང་།"
},
"room": {
"leave": "ཕྱིར་ཐོན།",
@ -148,7 +163,7 @@
"room_list_rooms": "ཁ་བརྡ་ཁང་།",
"room_list_invites": "གདན་ཞུ་ཁག",
"purge_failed": "ཁ་བརྡ་ཁང་བཤིག་ཐུབ་མ་སོང་།",
"purge_removing_members": "ཚོགས་མི་ཁག་ཕྱིར་འདོན། ({total}་ཀྱི་({count})",
"purge_removing_members": "ཚོགས་མི་ཁག་ཕྱིར་འདོན། {total})་ཀྱི་({members}",
"purge_redacting_events": "ཁ་བརྡ་གཙང་གསུབ། {total})་ཀྱི་({count}",
"purge_set_room_state": "ཁ་བརྡ་ཁང་གི་རྣམ་པ་སྒྲིག་འགོད།",
"room_list_new_messages": "{count} ཆ་འཕྲིན་གསར་པ།",
@ -201,14 +216,24 @@
"incoming_message_deleted_text": "ཆ་འཕྲིན་འདི་བསུབས་ཟིན།",
"reply_poll": "བསམ་ཚུལ་བསྡུ་ལེན།",
"not_allowed_to_send": "དོ་དམ་པ་དང་གཙོ་སྐྱོང་བ་ཁོ་ནས་མ་གཏོགས་ཁ་བརྡ་ཁང་དུ་གཏོང་མི་ཆོག",
"user_was_kicked_by_you": "ཁྱེད་ཀྱིས་{user}་འདི་ཁ་བརྡའི་ཁོངས་ནས་སྒོར་ཕུད་སོང་།",
"user_was_kicked": "{user} ་འདི་ཁ་བརྡའི་ཁོངས་ནས་སྒོར་ཕུད་ཟིན།",
"user_was_kicked_by_you": "ཁྱེད་ཀྱིས་{སྤྱོད་མཁན}་འདི་ཁ་བརྡའི་ཁོངས་ནས་སྒོར་ཕུད་སོང་།",
"user_was_kicked": "{སྤྱོད་མཁན} ་འདི་ཁ་བརྡའི་ཁོངས་ནས་སྒོར་ཕུད་ཟིན།",
"user_was_kicked_you": "ཁྱེད་རང་ཁ་བརྡའི་ཁོངས་ནས་སྒོར་ཕུད་སོང་།",
"user_was_banned": "{user}་འདི་ཁ་བརྡའི་ཁོངས་ནས་བཀག་སྡོམ་བྱས་ཏེ་སྒོར་ཕུད་ཟིན།",
"user_was_banned_by_you": "ཁྱེད་ཀྱིས་{user} ་འདི་ཁ་བརྡའི་ཁོངས་ནས་བཀག་སྡོམ་བྱས་ཏེ་སྒོར་ཕུད་ཟིན།",
"time_ago": "དེ་རིང་། | ཁ་སང་། | ཉིན་གྲངས་{count} གོང་།",
"outgoing_message_deleted_text": "ཁྱེད་ཀྱིས་ཆ་འཕྲིན་འདི་བསུབས་སོང་།",
"reaction_count_more": "{reactionCount} མང་བ།"
"reaction_count_more": "{reactionCount} མང་བ།",
"images": "པར་རིས།",
"send_attachements_dialog_title": "ཁྱེད་ཀྱིས་གཤམ་གྱི་ཟུར་སྣོན་འདི་དག་གཏོང་འདོད་དམ།",
"preparing_to_upload": "ཡར་འཇུག་བྱེད་པར་གྲ་སྒྲིག་བྱེད་བཞིན་པ...",
"seen_by": "མཐོང་མཁན།",
"seen_by_count": "ཚོགས་མི་སུས་ཀྱང་མཐོང་མི་འདུག| ཚོགས་མི་གཅིག་གིས་མཐོང་འདུག ཚོགས་མི་{count}་མཐོང་འདུག",
"files": "ཡིག་ཆ་ཁག",
"download_all": "ཆ་ཚང་ཕབ་ལེན།",
"file": "ཡིག་ཆ།",
"upload_file_too_large": "ཡར་འཇུག་བྱ་འདོད་ཀྱི་ཡིག་ཆ་ཆེ་དྲགས་འདུག",
"upload_exceeded_file_limit": "({configFormattedUploadSize}) ་ཡི་ཡིག་ཆ་ཆེ་ཤོས་ཀྱི་ཆེ་ཆུང་གི་ཚད་གཞི་ལས་བརྒལ་འདུག "
},
"power_level": {
"moderator": "མདོ་འཛིན་པ།",
@ -261,7 +286,8 @@
"join_user": "ཁ་བརྡ་འགོ་རྩོམ།",
"enter_room_user": "ཁ་བརྡ་འགོ་རྩོམ།",
"choose_name": "མིང་ཞིག་འདེམས་ཏེ་བཀོལ།",
"title_user": "དགའ་བསུ་ཞུ། ཁྱེད་རང་ཁ་བརྡ་བྱེད་པར་གདན་ཞུ་གནང་སོང་།"
"title_user": "དགའ་བསུ་ཞུ། ཁྱེད་རང་ཁ་བརྡ་བྱེད་པར་གདན་ཞུ་གནང་སོང་།",
"you_have_been_banned": "ཁྱེད་རང་ཚོགས་པ་འདིའི་ནང་ནས་བཀག་འདུག"
},
"profile_info_popup": {
"powered_by": "ཁ་བརྡ་ཁང་འདི་{product} ནུས་ཤུགས་བསྩལ་ཡོད། {productLink} ནས་དེ་ལས་མང་བ་སྦྱོང་ཆོག་ལ། མདུན་དུ་བསྐྱོད་དེ་ཁ་བརྡ་ཁང་གཞན་ཞིག་བསྐྲུན་ཆོག",
@ -282,11 +308,19 @@
"global": {
"save": "ཉར་ཚགས།",
"password_didnot_match": "གསང་ཚིག་མཐུན་གྱི་མི་འདུག",
"password_hint": "ཉུང་མཐར་ཡང་ཡིག་འབྲུ་༡༢་དགོས་ལ། དེའི་ནང་དུ་ཨང་གྲངས་གཅིག་དང་། ཡིག་ཆེན་གཅིག ཡིག་ཆུང་གཅིག་ངེས་པར་དུ་ཚང་དགོས།",
"password_hint": "ཉུང་མཐར་ཡང་ཡིག་འབྲུ་༡༢་ཀྱི་ཁོངས་སུ། ཨང་གྲངས་གཅིག་དང་། ཡིག་ཆེན་གཅིག ཡིག་ཆུང་གཅིག་ངེས་པར་དུ་ཚང་དགོས།",
"add_reaction": "ཡ་ལན་ཁ་སྣོན།",
"click_to_remove": "བསྣུན་ཏེ་མེད་པར་བཟོས།",
"show_less": "ཉུང་བ་སྟོན།",
"show_more": "མང་བ་སྟོན།"
"show_more": "མང་བ་སྟོན།",
"time": {
"hours": "ཆུ་ཚོད་གཅིག་གི་སྔོན། | ཆུ་ཚོད {n} སྔོན་ལ།",
"minutes": "སྐར་མ་གཅིག་གི་སྔོན། | སྐར་མ {n} སྔོན་ལ།",
"days": "ཉིན་གཅིག་གི་གོང་། | ཉིན{n}གོང་ལ།",
"recently": "ད་ལྟ་རང་།"
},
"notify": "བརྡ་ཁྱབ་གཏོང་བ།",
"close": "སྒོ་རྒྱོབ།"
},
"logout": {
"confirm_text": "ཁྱེད་རང་ཁ་བརྡ་ཁང་ནས་ཕྱི་རུ་ཐོན་རྒྱུ་ཡིན་ནམ།"
@ -321,6 +355,57 @@
"export": {
"exported_date": "{date} ཉིན་ལ་ཕྱིར་འདྲེན་བྱས།",
"export_filename": "{date} ཉིན་ཕྱིར་འདྲེན་བྱས་པའི་ཁ་བརྡ།",
"processed_n_of_total_events": "བྱུང་བ་{total}ཁོངས་ནས་ལས་སྣོན་བྱས་ཟིན་པའི་སྨྱན་སྦྱོར་གྱི་གྲངས {count}"
"processed_n_of_total_events": "བྱུང་བ་{total}ཁོངས་ནས་ལས་སྣོན་བྱས་ཟིན་པའི་སྨྱན་སྦྱོར་གྱི་གྲངས {count}",
"fetched_n_of_total_events": "གནས་ཚུལ་{total} ནང་ནས་{count} བཏོན་གནང་།",
"fetched_n_events": "བཏོན་གནང་བའི་གནས་ཚུལ{count}"
},
"file_mode": {
"add_a_message": "འཕྲིན་ཐུང་ཞིག་ཁ་སྣོན་བྱོས།",
"send_more_files": "ཡིག་ཆ་དེ་ལས་མང་བ་ཐོངས།",
"choose_files": "ཡིག་ཆ་འདེམས།",
"secure_file_send": "བདེ་འཇགས་ཡིག་ཆ་བརྒྱུད་གཏོང་།",
"sending": "གཏོང་བཞིན་པ།",
"files": "ཡིག་ཆ་ཁག",
"sending_progress": "གཏོང་བཞིན་པ...",
"any_file_format_accepted": "ཡིག་ཆའི་རྣམ་བཞག་ཆ་ཚང་ངོས་ལེན་བྱེད།",
"files_sent_with_note": "མཆན་ཞིག་དང་མཉམ་དུ་ཡིག་ཆ་1་བཏང་ཟིན། མཆན་ཞིག་དང་མཉམ་དུ་ཡིག་ཆ {count} བཏང་ཟིན།",
"close": "སྒོ་རྒྱོབ།",
"files_sent": "ཡིག་ཆ1 བཏང་ཟིན། |ཡིག་ཆ{count}བཏང་ཟིན།"
},
"notification": {
"dialog": {
"body": "འཕྲིན་ཐུང་དང་ཁ་བརྡ་གལ་ཆེན་གཅིག་ཀྱང་མ་ཤོར་བ་བྱོས། མི་ཞིག་གིས་ཁྱེད་ལ་འཕྲིན་ཐུང་གཏོང་སྐབས་སམ། ཁྱེད་ཀྱི་ཁ་བརྡ་ལ་ལན་འདེབས་གནང་སྐབས་བརྡ་ཁྱབ་གཏོང་ངེས།",
"title": "གླེང་མོལ་བརྡ་ཁྱབ་ལ་འབྲེལ་མཐུད་རྒྱུན་འཁྱོངས་བྱོས།",
"enable": "ནུས་ཡོད་བཟོ་བ།"
},
"title": "འཕྲིན་ཐུང་གསར་པ་འབྱོར་སོང་།",
"blocked_message": "བརྡ་ཁྱབ་བཀག་འདུག ཆོག་མཆན་ཁག་བསྐྱར་སྒྲིག་བྱེད་རོགས།"
},
"emoji": {
"categories": {
"nature": "རང་བྱུང་།",
"places": "ས་ཆ།",
"activity": "བྱེད་སྒོ།",
"flags": "དར་ཆ་ཁག",
"foods": "ཞལ་ཟས།",
"objects": "བྱ་དངོས།",
"peoples": "མི་མང་།",
"symbols": "མཚོན་རྟགས།",
"frequently": "རྒྱུན་དུ་སྤྱོད་བཞིན་པ།"
},
"search": "འཚོལ..."
},
"getlink": {
"title": "ཐད་ཀར་འབྲེལ་ཐག་ཐོབ་པར་བྱོས།",
"info": "ཐད་ཀར་ཁ་བརྡ་བྱེད་སྤྱད་ཀྱི་འབྲེལ་ཐག་གིས་ཁྱེད་དང་མི་གཞན་གྱི་བར་ལ་བདེ་འཇགས་ཀྱི་འཕྲིན་གཏོང་སྐུད་ལམ་ཞིག་མཁོ་སྤྲོད་བྱེད། འགོ་འཛུགས་བྱེད་པར། མི་ཚོས་ཁྱེད་ལ་ཁ་བརྡ་བྱེད་པར་སླེབས་དུས། འཆར་མིང་ཞིག་འདེམས་ཏེ་སྟོན།",
"share_qr": "བཤེར་རིས་བརྒྱུད་སྐུར་བྱོས།",
"next": "རྗེས་མ།",
"scan_title": "བཤེར་རིས་འདི་བཤེར་ཏེ་ཐད་ཀར་ཁ་བརྡ་ཞིག་འགོ་རྩོམ།",
"continue": "མུ་མཐུད།",
"ready_to_share": "བརྒྱུད་སྐུར་བྱེད་པར་གྲ་སྒྲིག་ཡིན། མི་ཞིག་གིས་འབྲེལ་ཐག་དེ་ཁ་འབྱེད་ཐེངས་རེར། ཐད་ཀར་གླེང་མོལ་བྱ་སའི་ཁ་བརྡ་ཁང་ཞིག་ཁ་འབྱེད་ངེས།",
"qr_image_copied": "འབྲེག་པང་གི་སྟེང་དུ་པར་རིས་འདྲ་བཤུས་བྱས་ཚར།",
"hello": "ཁམས་བཟང་། {user},\nའདི་ཐད་ཀར་ཁ་བརྡ་བྱེད་སའི་ཁྱེད་ཀྱི་འབྲེལ་ཐག་ཡིན།",
"username": "འཆར་མིང་ཞིག་གཏགས། (དཔེར་ན།: waku)",
"different_link": "འབྲེལ་ཐག་གཞན་པ་ཞིག་རག་པར་བྱོས།"
}
}

View file

@ -262,5 +262,12 @@
"processed_n_of_total_events": "Medien für {count} von {total} Ereignissen verarbeitet",
"fetched_n_events": "{count} Ereignisse geladen",
"export_filename": "Chat exportiert: {date}"
},
"global": {
"save": "Speichern",
"show_more": "Mehr anzeigen",
"password_didnot_match": "Passwort stimmt nicht überein",
"add_reaction": "Reaktion hinzufügen",
"show_less": "Weniger anzeigen"
}
}

View file

@ -23,6 +23,7 @@
},
"menu": {
"start_private_chat": "Direct Message with this user",
"actions": "actions",
"reply": "Reply",
"edit": "Edit",
"delete": "Delete",
@ -313,6 +314,20 @@
"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",
"message_retention": "Disappearing messages",
"message_retention_info": "You can set a timeout for automatically removing messages from the room.\nNote: the server must support this feature.",
"message_retention_none": "Off",
"message_retention_1_hour": "1 Hour",
"message_retention_1_day": "1 Day",
"message_retention_1_week": "1 Week",
"message_retention_30_days": "30 Days",
"message_retention_365_days": "365 Days",
"message_retention_other": "Other",
"message_retention_other_seconds": "{count} Seconds",
"message_retention_other_seconds_unit": "Seconds",
"message_retention_other_required": "Value can not be empty",
"message_retention_other_too_small": "Value must be at least {count} seconds",
"message_retention_other_too_large": "Value can be at most {count} seconds",
"make_public": "Make Public",
"make_public_warning": "warning: Full message history will be visible to new participants",
"direct_link": "My Direct Link",

View file

@ -45,7 +45,7 @@
"info": "Todos los miembros y mensajes serán eliminados. Esta acción no se puede deshacer.",
"title": "¿Borrar la sala?",
"n_seconds": "{seconds} segundos",
"self_destruct": "La habitación se autodestruirá en segundos.",
"self_destruct": "Tu sala se autodestruirá en segundos.",
"deleting": "Borrando la sala:",
"notified": "Hemos avisado a los miembros.",
"room_deletion_notice": "¡Es hora de decir adiós! Esta sala ha sido eliminada por {user}. Se autodestruirá en segundos."
@ -98,7 +98,8 @@
"language_description": "Convine esta disponible en varios Idiomas.",
"dont_see_yours": "¿No ves el tuyo?",
"tell_us": "Dinos.",
"display_name_required": "El nombre para mostrar es obligatorio"
"display_name_required": "El nombre para mostrar es obligatorio",
"notification_label": "Notificación"
},
"login": {
"login": "Iniciar sesión",
@ -148,7 +149,8 @@
"create": "Crear",
"room_topic": "Añade una descripción si quieres",
"options": "Opciones",
"room_name_limit_error_msg": "50 caracteres como máximo"
"room_name_limit_error_msg": "50 caracteres como máximo",
"colon_not_allowed": "Colon no está permitido"
},
"room_welcome": {
"join_public": "Cualquiera puede unirse abriendo este vínculo: {link}.",
@ -239,7 +241,9 @@
"user_was_kicked_by_you": "Has expulsado a {user} del chat.",
"upload_file_too_large": "¡El archivo es demasiado grande para subirlo!",
"time_ago": "Hoy | Ayer | Hace {count} días",
"upload_exceeded_file_limit": "Se ha superado el tamaño máximo para el archivo ({configFormattedUploadSize}). "
"upload_exceeded_file_limit": "Se ha superado el tamaño máximo para el archivo ({configFormattedUploadSize}). ",
"someone": "Alguien",
"sent_media": "Enviados {count} elementos multimedia."
},
"menu": {
"login": "Iniciar sesión",
@ -263,7 +267,8 @@
"user_kick_and_ban": "Expulsar y banear a este usuario",
"user_make_moderator": "Hacer moderador",
"user_make_admin": "Hacer administrador",
"user_revoke_moderator": "Revocar al moderador"
"user_revoke_moderator": "Revocar al moderador",
"delete_now": "Elimina ahora"
},
"fallbacks": {
"download_name": "Descargar",
@ -395,7 +400,8 @@
"title": "¡Manténgase conectado con las notificaciones para el chat!"
},
"title": "Nuevo mensaje recibido",
"blocked_message": "Notificaciones bloqueadas. Por favor, restablezca los permisos"
"blocked_message": "La notificación está bloqueada. Vaya a la configuración de su dispositivo o navegador para habilitar la Notificación",
"not_supported": "La notificación aún no es compatible con dispositivos móviles"
},
"export": {
"fetched_n_of_total_events": "{count} de {total} eventos recuperados",

View file

@ -56,7 +56,8 @@
"user_make_moderator": "Tornar moderador",
"user_kick_and_ban": "Expulsar e banir este usuário",
"user_make_admin": "Tornar administrador",
"user_revoke_moderator": "Revogar moderador"
"user_revoke_moderator": "Revogar moderador",
"delete_now": "Excluir agora"
},
"message": {
"you": "Você",
@ -119,7 +120,9 @@
"files": "Arquivos",
"download_all": "Baixar tudo",
"upload_exceeded_file_limit": "O tamanho máximo do arquivo ({configFormattedUploadSize}) foi excedido. ",
"preparing_to_upload": "Preparando para enviar..."
"preparing_to_upload": "Preparando para enviar...",
"someone": "Alguém",
"sent_media": "Enviou {count} itens de mídia."
},
"room": {
"members": "sem membros | 1 membro | {count} membros",
@ -215,7 +218,8 @@
"password_new": "Nova senha",
"password_repeat": "Repita a nova senha",
"display_name": "Nome de exibição",
"display_name_required": "O nome de exibição é obrigatório"
"display_name_required": "O nome de exibição é obrigatório",
"notification_label": "Notificação"
},
"profile_info_popup": {
"you_are": "Você é",
@ -261,7 +265,7 @@
"info": "Todos os membros e as mensagens serão excluídos. Essa ação não pode ser desfeita.",
"button": "Excluir",
"n_seconds": "{seconds} segundos",
"self_destruct": "A sala se autodestruirá em segundos.",
"self_destruct": "A sua sala se autodestruirá em segundos.",
"deleting": "Excluindo a sala:",
"notified": "Nós notificamos os membros.",
"room_deletion_notice": "Hora de dizer adeus! Esta sala foi excluída pelo {user}. Ela se autodestruirá em segundos."
@ -393,7 +397,8 @@
"title": "Manter-se conectado com as notificações do chat!",
"enable": "Ativar"
},
"blocked_message": "Notificações bloqueadas. Redefina as permissões"
"blocked_message": "A notificação está bloqueada. Vá para as configurações do dispositivo ou do navegador para ativar a notificação",
"not_supported": "A notificação ainda não é suportada no mobile"
},
"getlink": {
"title": "Obter um link direto",

View file

@ -34,7 +34,16 @@
"read_only_room_info": "只允许管理员和版主发送到聊天室",
"export_room": "导出聊天",
"user_moderator": "版主",
"experimental_features": "实验功能"
"experimental_features": "实验功能",
"direct_link": "我的直接链接",
"make_public_warning": "警告:新参与者将可见完整的信息历史记录",
"room_type": "聊天室种类",
"file_mode_info": "将聊天界面切换到“文件投送”模式",
"room_type_default": "默认",
"file_mode": "文件模式",
"direct_link_desc": "已准备好分享了! 每次有人打开链接时,会打开一个新的直接聊天室。",
"copy_link": "复制链接",
"make_public": "公开"
},
"leave": {
"leave": "离开",
@ -63,7 +72,10 @@
"email_not_valid": "电子邮件地址无效",
"terms": "主服务器要求您查看并接受以下政策:",
"accept_terms": "接受",
"email": "您需要验证您的电子邮件地址"
"email": "您需要验证您的电子邮件地址",
"registration_token": "请输入注册令牌",
"token_not_valid": "令牌无效",
"send_token": "发送令牌"
},
"device_list": {
"title": "设备",
@ -137,7 +149,17 @@
"outgoing_message_deleted_text": "你删除了这条信息。",
"incoming_message_deleted_text": "这条信息已删除。",
"not_allowed_to_send": "只允许管理员和版主发送到聊天室",
"reaction_count_more": "{reactionCount} 更多"
"reaction_count_more": "{reactionCount} 更多",
"images": "图片",
"send_attachements_dialog_title": "您想发送以下附件吗?",
"preparing_to_upload": "准备上传...",
"seen_by": "查看者",
"seen_by_count": "没有被群成员看到 |已被 1 位成员查看 | 已被 {count} 位成员查看",
"files": "文件",
"download_all": "全部下载",
"file": "文件",
"upload_file_too_large": "文件太大, 无法上传!",
"upload_exceeded_file_limit": "超出 ({configFormattedUploadSize}) 的最大文件大小。 "
},
"menu": {
"login": "登录",
@ -211,7 +233,8 @@
"title_user": "欢迎您被邀请聊天",
"join_user": "开始聊天",
"enter_room_user": "开始聊天",
"choose_name": "选择要使用的名称"
"choose_name": "选择要使用的名称",
"you_have_been_banned": "您已被禁止进入该聊天室。"
},
"profile": {
"display_name": "显示名称",
@ -261,7 +284,10 @@
"join_public": "任何人都可以加入通过打开此链接: {link}。",
"room_history_joined": "只有加入后,人们才可以看到发送的信息。",
"room_history_is": "聊天室纪录是{type}.",
"encrypted": "信息是端到端加密的。"
"encrypted": "信息是端到端加密的。",
"no_past_messages": "欢迎! 为了您的安全,过去的信息不提供。",
"direct_info": "你好,{you}.正在与 {user} 进行私人聊天。",
"direct_private_chat": "私信"
},
"profile_info_popup": {
"new_room": "新的聊天室",
@ -282,15 +308,19 @@
"global": {
"save": "保存",
"password_didnot_match": "密码不匹配",
"password_hint": "至少 12 个字符,包含至少一个数字、一个大写字母和一个小写字母",
"password_hint": "至少 12 个字符,包含一个数字、一个大写字母和一个小写字母",
"show_less": "显示较少",
"show_more": "展示更多",
"add_reaction": "添加反应",
"click_to_remove": "点击删除",
"time": {
"recently": "刚才"
"recently": "刚才",
"hours": "1小时前| {n}小时前",
"minutes": "1 分钟前 | {n} 分钟前",
"days": "1天前| {n}天前"
},
"close": "关闭"
"close": "关闭",
"notify": "通知"
},
"logout": {
"confirm_text": "您确定要注销吗?"
@ -328,5 +358,54 @@
"fetched_n_of_total_events": "已获取 {count} 个事件,共 {total} 个事件",
"processed_n_of_total_events": "已处理 {count} 个事件的媒体,共 {total} 个事件",
"export_filename": "导出的聊天 {date}"
},
"file_mode": {
"add_a_message": "添加留言",
"send_more_files": "发送更多文件",
"choose_files": "地方",
"secure_file_send": "安全文件发送",
"sending": "发送中",
"files": "文件",
"sending_progress": "正在发送...",
"any_file_format_accepted": "接受任何文件格式",
"files_sent_with_note": "发送 1 个文件并附有备注! | 已发送 {count} 个文件并附有备注!",
"close": "关闭",
"files_sent": "已发送 1 个文件! | 已发送 {count} 个文件!"
},
"notification": {
"dialog": {
"body": "再也不要错过任何信息或重要对话! 每当有人向您发送信息或回复您的聊天时,您会收到通知。",
"title": "通过聊天通知保持联系!",
"enable": "启用"
},
"title": "收到新信息",
"blocked_message": "通知被屏蔽。 请重置权限"
},
"emoji": {
"categories": {
"nature": "自然",
"places": "地方",
"activity": "活动",
"flags": "旗帜",
"foods": "食品",
"objects": "物体",
"peoples": "人民",
"symbols": "符号",
"frequently": "常用的"
},
"search": "搜索..."
},
"getlink": {
"title": "获取直接链接",
"info": "直接链接为人们与您提供安全的沟通渠道。首先,选择当人们与您聊天时显示的屏幕名称。",
"share_qr": "扫描二维码",
"next": "下一步",
"scan_title": "扫描此二维码即可直接聊天",
"continue": "继续",
"ready_to_share": "已准备好分享了! 每次有人打开链接时,会打开一个新的直接聊天室。",
"qr_image_copied": "图片已复制到剪贴板",
"hello": "你好{user}\n这是您的直接链接",
"username": "输入屏幕名称例如waku",
"different_link": "获取其它链接"
}
}

View file

@ -41,7 +41,9 @@
v-on:download="download(selectedEvent)" v-on:more="
isEmojiQuickReaction= true
showMoreMessageOperations({event: selectedEvent, anchor: $event.anchor})
" :originalEvent="selectedEvent" :timelineSet="timelineSet" />
" :originalEvent="selectedEvent" :timelineSet="timelineSet"
:readOnlyRoom="$matrix.currentRoomIsReadOnlyForUser"
/>
</div>
<div ref="avatarOperationsStrut" class="avatar-operations-strut">
@ -483,6 +485,11 @@ export default {
Places: this.$t("emoji.categories.places")
}
},
/**
* A timer to handle message retention/auto deletion
*/
retentionTimer: null
};
},
@ -511,6 +518,10 @@ export default {
this.$root.$off('audio-playback-ended', this.audioPlaybackEnded);
this.$audioPlayer.pause();
this.stopRRTimer();
if (this.retentionTimer) {
clearInterval(this.retentionTimer);
this.retentionTimer = null;
}
},
destroyed() {
@ -774,6 +785,12 @@ export default {
this.events.filter(event => (event.threadRootId && !event.parentThread)).forEach(event => this.setParentThread(event));
this.events.filter(event => (event.replyEventId && !event.replyEvent)).forEach(event => this.setReplyToEvent(event));
console.log("Loading finished!");
this.updateRetentionTimer();
} else if (!value) {
if (this.retentionTimer) {
clearInterval(this.retentionTimer);
this.retentionTimer = null;
}
}
}
},
@ -871,6 +888,52 @@ export default {
this.initialLoadDone = true;
console.log("Loading finished!");
},
/**
* Set events to display. At the same time, filter out messages that are past rentention period etc.
*/
setEvents(events) {
this.events = this.removeTimedOutEvents(events);
},
updateRetentionTimer(maybeEvent) {
const retentionEvent = maybeEvent || (this.room && this.room.currentState.getStateEvents("m.room.retention", ""));
if (retentionEvent) {
const maxLifetime = parseInt(retentionEvent.getContent().max_lifetime);
if (maxLifetime) {
if (!this.retentionTimer) {
this.retentionTimer = setInterval(this.onRetentionTimer, 60000);
}
return;
}
}
if (this.retentionTimer) {
clearInterval(this.retentionTimer);
this.retentionTimer = null;
}
},
removeTimedOutEvents(events) {
const retentionEvent = this.room && this.room.currentState.getStateEvents("m.room.retention", "");
let maxLifetime = 0;
if (retentionEvent) {
maxLifetime = parseInt(retentionEvent.getContent().max_lifetime);
}
return events.filter((e) => {
if (maxLifetime > 0 && !e.isState()) { // Keep all state events
return e.getLocalAge() < maxLifetime;
}
return true;
});
},
onRetentionTimer() {
const events = this.removeTimedOutEvents(this.events);
if (events.length != this.events.length) {
this.events = events; // Changed
}
},
onRoomJoined(initialEventId) {
// Listen to events
this.$matrix.on("Room.timeline", this.onEvent);
@ -885,7 +948,7 @@ export default {
this.timelineWindow
.load(initialEventId, 20)
.then(() => {
self.events = self.timelineWindow.getEvents();
self.setEvents(self.timelineWindow.getEvents());
const getMoreIfNeeded = function _getMoreIfNeeded() {
const container = self.$refs.chatContainer;
@ -896,7 +959,7 @@ export default {
self.timelineWindow.canPaginate(EventTimeline.BACKWARDS)
) {
return self.timelineWindow.paginate(EventTimeline.BACKWARDS, 10, true, 5).then((success) => {
self.events = self.timelineWindow.getEvents();
self.setEvents(self.timelineWindow.getEvents());
if (success) {
return _getMoreIfNeeded.call(self);
} else {
@ -943,7 +1006,7 @@ export default {
this.onRoomJoined(null);
} else {
// Error. Done loading.
this.events = this.timelineWindow.getEvents();
this.setEvents(this.timelineWindow.getEvents());
this.setInitialLoadDone();
}
})
@ -976,7 +1039,7 @@ export default {
.then(() => {
self.timelineSet = timelineSet;
self.timelineWindow = timelineWindow;
self.events = self.timelineWindow.getEvents();
self.setEvents(self.timelineWindow.getEvents());
})
.finally(() => {
this.loading = false;
@ -1090,7 +1153,7 @@ export default {
if (tl) {
const parentEvent = tl.getEvents().find((e) => e.getId() === event.threadRootId);
if (parentEvent) {
this.events = this.timelineWindow.getEvents();
this.setEvents(this.timelineWindow.getEvents());
const fn = () => {
Vue.set(parentEvent, "isMxThread", true);
Vue.set(event, "parentThread", parentEvent);
@ -1123,7 +1186,7 @@ export default {
if (tl) {
const parentEvent = tl.getEvents().find((e) => e.getId() === event.replyEventId);
if (parentEvent) {
this.events = this.timelineWindow.getEvents();
this.setEvents(this.timelineWindow.getEvents());
const fn = () => {Vue.set(event, "replyEvent", parentEvent);};
if (this.initialLoadDone) {
const sel = "[eventId=\"" + parentEvent.getId() + "\"]";
@ -1162,7 +1225,7 @@ export default {
this.paginateBackIfNeeded();
}
if (loadingDone && event.forwardLooking && (!event.isRelation() || event.isMxThread || event.threadRootId || event.parentThread )) {
if (loadingDone && event.forwardLooking && (!event.isRelation() || event.isMxThread || event.threadRootId || event.parentThread)) {
// If we are at bottom, scroll to see new events...
var scrollToSeeNew = event.getSender() == this.$matrix.currentUserId; // When we sent, scroll
const container = this.chatContainer;
@ -1179,13 +1242,17 @@ export default {
(event.getPrevContent() || {}).membership == "join" &&
(
(event.getContent().membership == "leave" && event.getSender() != this.currentUserId) ||
(event.getContent().membership == "ban" ))
) {
this.$store.commit("setCurrentRoomId", null);
const wasPurged = event.getContent().reason == "Room Deleted";
this.$navigation.push({ name: "Goodbye", params: { roomWasPurged: wasPurged } }, -1);
}
(event.getContent().membership == "ban"))
) {
this.$store.commit("setCurrentRoomId", null);
const wasPurged = event.getContent().reason == "Room Deleted";
this.$navigation.push({ name: "Goodbye", params: { roomWasPurged: wasPurged } }, -1);
}
else if (event.getType() === "m.room.retention") {
this.updateRetentionTimer(event);
}
}
},
onUserTyping(event, member) {
@ -1404,7 +1471,7 @@ export default {
.then((success) => {
if (success && this.scrollPosition) {
this.scrollPosition.prepareFor("up");
this.events = this.timelineWindow.getEvents();
this.setEvents(this.timelineWindow.getEvents());
this.$nextTick(() => {
// restore scroll position!
console.log("Restore scroll!");
@ -1429,7 +1496,7 @@ export default {
.paginate(EventTimeline.FORWARDS, 10, true)
.then((success) => {
if (success) {
this.events = this.timelineWindow.getEvents();
this.setEvents(this.timelineWindow.getEvents());
if (!this.useVoiceMode && this.scrollPosition) {
this.scrollPosition.prepareFor("down");
this.$nextTick(() => {

View file

@ -477,14 +477,25 @@ export default {
// Set power level event. Need to do that here, because we might not have the userId when the options object is created.
const powerLevels = {};
powerLevels[this.$matrix.currentUserId] = 100;
let powerLevelContent = {
users: powerLevels,
events_default: this.readOnlyRoom ? 50 : 0
}
if (this.readOnlyRoom) {
powerLevelContent.events = {
"m.room.encrypted": 0, // NOTE! Since practically all events in encrypted rooms get sent as "m.room.encrypted" we need to set
// power to 0 here. Otherwise we would not be able to send quick reactions or poll responses...
"m.poll.response": 0,
"org.matrix.msc3381.poll.response": 0,
"m.reaction": 0,
"m.room.redaction": 0,
};
}
createRoomOptions.initial_state.push(
{
type: "m.room.power_levels",
state_key: "",
content: {
users: powerLevels,
events_default: this.readOnlyRoom ? 50 : 0
}
content: powerLevelContent
});
return this.$matrix.matrixClient

View file

@ -1,6 +1,5 @@
<template>
<v-list dense @click.native.stop="nullEvent">
<v-subheader>{{$t('device_list.title')}}</v-subheader>
<v-list-item-group color="primary">
<v-list-item
v-for="device in devices"

View file

@ -0,0 +1,219 @@
<template>
<v-dialog persistent v-model="showDialog" v-show="room" class="ma-0 pa-0"
:width="$vuetify.breakpoint.smAndUp ? '688px' : '95%'">
<div class="dialog-content text-center">
<template>
<h2 class="dialog-title">{{ $t("room_info.message_retention") }}</h2>
<div class="dialog-text">
{{ $t("room_info.message_retention_info") }}
</div>
</template>
<v-container fluid>
<v-row cols="12">
<v-col cols="12">
<v-form v-model="isValid">
<v-radio-group v-model="retention" :value-comparator="compareValue">
<v-radio active-class="radio-active" v-for="p in retentionPeriods" :key="p.text" :label="p.text"
:value="p.value">
<template v-slot:label v-if="p.value === ''">
<div :class="{ 'other': true, 'visible': selectedOther }"><span>{{ p.text }}</span>
<v-text-field :show="selectedOther" ref="otherInput" background-color="transparent" height="20px"
dense outlined x-hide-details autofocus v-model="retentionOther" single-line type="number"
hide-spin-buttons :rules="otherInputRules"
:suffix="$t('room_info.message_retention_other_seconds_unit')"></v-text-field>
</div>
</template>
</v-radio>
</v-radio-group>
</v-form>
</v-col>
</v-row>
<v-row cols="12">
<v-col cols="6">
<v-btn depressed text block class="text-button" @click="showDialog = false">{{
$t("menu.cancel") }}</v-btn>
</v-col>
<v-col cols="6" align="center">
<v-btn color="primary" :disabled="!isValid" depressed block class="filled-button"
@click.stop="setRetention()">{{
$t("menu.ok") }}</v-btn>
</v-col>
</v-row>
</v-container>
</div>
</v-dialog>
</template>
<script>
import roomInfoMixin from "./roomInfoMixin";
export default {
name: "MessageRetentionDialog",
mixins: [roomInfoMixin],
props: {
show: {
type: Boolean,
default: function () {
return false;
},
},
},
data() {
return {
showDialog: false,
isValid: true,
selectedOther: false,
retention: 0,
retentionOther: 60, // Set same as min below
retentionMinValue: 60,
retentionMaxValue: 60 * 60 * 24 * 500,
retentionPeriods: [
{
text: this.$t("room_info.message_retention_none"),
value: 0
},
{
text: this.$t("room_info.message_retention_1_hour"),
value: 3600 * 1000
},
{
text: this.$t("room_info.message_retention_1_day"),
value: 3600 * 24 * 1000
},
{
text: this.$t("room_info.message_retention_30_days"),
value: 3600 * 24 * 30 * 1000
},
{
text: this.$t("room_info.message_retention_365_days"),
value: 3600 * 24 * 365 * 1000
},
{
text: this.$t("room_info.message_retention_other"),
value: ''
},
],
otherInputRules: [
v => !!v || this.$t("room_info.message_retention_other_required"),
v => (v && v >= this.retentionMinValue) || this.$t("room_info.message_retention_other_too_small", { count: this.retentionMinValue }),
v => (v && v <= this.retentionMaxValue) || this.$t("room_info.message_retention_other_too_large", { count: this.retentionMaxValue }),
]
};
},
watch: {
show: {
handler(newVal, ignoredOldVal) {
this.showDialog = newVal;
},
},
showDialog(val, oldVal) {
if (!val && oldVal) {
this.$emit("close");
} else if (val && !oldVal) {
// Showing, reset
this.setInitialValue();
}
},
retention(val) {
if (val === '') {
this.selectedOther = true;
this.retentionOther = this.retentionMaxValue;
this.$nextTick(() => {
this.$refs.otherInput[0].focus();
});
} else {
// If one of the other, it's not "other"
for (var i = 0; i < this.retentionPeriods.length; i++) {
if (this.retentionPeriods[i].value === val) {
this.selectedOther = false;
}
}
}
}
},
mounted() {
this.setInitialValue();
},
methods: {
setInitialValue() {
this.isValid = true;
this.retention = this.roomMessageRetention();
let found = false;
for (var i = 0; i < this.retentionPeriods.length; i++) {
if (this.retentionPeriods[i].value !== '' && this.retentionPeriods[i].value === this.retention) {
found = true;
break;
}
}
if (!found) {
this.selectedOther = true;
}
},
compareValue(a, b) {
if (this.selectedOther) {
if (b === '') {
return true;
}
return false;
}
if (a === b) return true;
if (b === '') {
// If none of the other, it's "other"
for (var i = 0; i < this.retentionPeriods.length; i++) {
if (this.retentionPeriods[i].value === a) {
return false;
}
}
return true;
}
return false;
},
setRetention() {
let ms = this.retention;
if (this.selectedOther) {
ms = this.retentionOther * 1000;
}
if (ms === 0 || !ms) {
// No expiry
this.$matrix.matrixClient.sendStateEvent(
this.room.roomId,
"m.room.retention",
{ max_lifetime: 0 }
);
this.showDialog = false;
} else if (ms >= 1000 * this.retentionMinValue && ms <= 1000 * this.retentionMaxValue) {
this.$matrix.matrixClient.sendStateEvent(
this.room.roomId,
"m.room.retention",
{ max_lifetime: ms }
);
this.showDialog = false;
}
}
},
};
</script>
<style lang="scss">
@import "@/assets/css/chat.scss";
.v-radio:last-of-type {
align-items: start !important;
}
.v-radio .other {
display: flex;
min-height: 80px;
}
.v-radio .v-text-field {
display: none;
margin-left: 8px;
min-width: 200px;
}
.v-radio.radio-active {
.v-text-field {
display: inline-flex;
}
}
</style>

View file

@ -138,7 +138,7 @@
</v-card-text>
</v-card>
<v-card class="account ma-3" flat v-if="(iAmAdmin() && availableRoomTypes.length > 1) || canChangeReadOnly()">
<v-card class="account ma-3" flat v-if="(iAmAdmin() && availableRoomTypes.length > 1) || canChangeReadOnly() || canViewRetentionPolicy">
<v-card-title class="h2 with-right-label"><div>{{ $t("room_info.experimental_features") }}</div><div></div></v-card-title>
<v-card-text v-if="iAmAdmin() && availableRoomTypes.length > 1">
<div class="d-flex flex-wrap">
@ -159,6 +159,14 @@
v-model="readOnlyRoom"
></v-switch>
</v-card-text>
<v-card-text class="with-right-label" style="align-items:center" v-if="canViewRetentionPolicy">
<div>
<div class="option-title">{{ $t('room_info.message_retention') }}</div>
<div class="option-text">{{ $t('room_info.message_retention_info') }}</div>
</div>
<div class="text-right">{{ messageRetention }}</div>
<v-btn v-on:click="showMessageRetentionDialog = true" v-if="canChangeRetentionPolicy" icon size="x-small"><v-icon color="black">edit</v-icon></v-btn>
</v-card-text>
</v-card>
<v-card class="members ma-3" flat>
@ -166,58 +174,72 @@
>{{ $t("room_info.members") }}<v-spacer></v-spacer>
<div>{{ members.length }}</div></v-card-title
>
<v-card-text>
<div
class="member ma-2"
v-for="(member, index) in members"
:key="member.userId"
v-show="showAllMembers || index < SHOW_MEMBER_LIMIT"
@click="toggleMemberExpanded(member)"
>
<v-avatar class="avatar" size="32" color="grey">
<img v-if="memberAvatar(member)" :src="memberAvatar(member)" />
<span v-else class="white--text headline">{{
member.name.substring(0, 1).toUpperCase()
}}</span>
</v-avatar>
<span class="user-name">
{{
member.userId == $matrix.currentUserId
? $t("room_info.user_you", {
user: member.user ? member.user.displayName : member.name,
})
: $t("room_info.user", {
user: member.user ? member.user.displayName : member.name,
})
}}
</span>
<span v-if="isAdmin(member)" class="user-power">
{{ $t("room_info.user_admin") }}
</span>
<span v-else-if="isModerator(member)" class="user-power">
{{ $t("room_info.user_moderator") }}
</span>
<div v-if="member.userId != $matrix.currentUserId && !$matrix.isDirectRoomWith(room, member.userId) && expandedMembers.includes(member)" class="start-private-chat clickable" @click="startPrivateChat(member.userId)">{{ $t("menu.start_private_chat") }}</div>
<div v-if="canKickUser(member) || canBanUser(member)">
<div v-if="member.userId != $matrix.currentUserId && expandedMembers.includes(member)" class="mt-2">{{ String.fromCharCode(160) }}</div>
<div v-if="member.userId != $matrix.currentUserId && expandedMembers.includes(member) && canKickUser(member)" class="start-private-chat clickable" @click="kickUser(member)">{{ $t("menu.user_kick") }}</div>
<div v-if="member.userId != $matrix.currentUserId && expandedMembers.includes(member) && canBanUser(member)" class="start-private-chat clickable" @click="banUser(member)">{{ $t("menu.user_kick_and_ban") }}</div>
</div>
<div v-if="member.userId != $matrix.currentUserId && expandedMembers.includes(member)" class="mt-2">{{ String.fromCharCode(160) }}</div>
<div v-if="member.userId != $matrix.currentUserId && expandedMembers.includes(member) && !isAdmin(member) && canMakeAdmin(member)" class="start-private-chat clickable" @click="makeAdmin(member)">{{ $t("menu.user_make_admin") }}</div>
<div v-if="member.userId != $matrix.currentUserId && expandedMembers.includes(member) && !isModerator(member) && !isAdmin(member) && canMakeModerator(member)" class="start-private-chat clickable" @click="makeModerator(member)">{{ $t("menu.user_make_moderator") }}</div>
<div v-if="member.userId != $matrix.currentUserId && expandedMembers.includes(member) && isModerator(member) && canRevokeModerator(member)" class="start-private-chat clickable" @click="revokeModerator(member)">{{ $t("menu.user_revoke_moderator") }}</div>
<DeviceList
v-if="expandedMembers.includes(member)"
:member="member"
/>
</div>
<div class="show-all" @click="showAllMembers = !showAllMembers" v-if="members.length > SHOW_MEMBER_LIMIT">
{{
showAllMembers ? $t("room_info.hide_all") : $t("room_info.show_all")
}}
</div>
</v-card-text>
<v-expansion-panels>
<v-expansion-panel v-for="(member, index) in members" :key="member.userId">
<v-expansion-panel-header
class="member"
v-show="showAllMembers || index < SHOW_MEMBER_LIMIT"
>
<div>
<v-avatar class="avatar" size="32" color="grey">
<img v-if="memberAvatar(member)" :src="memberAvatar(member)" />
<span v-else class="white--text headline">{{
member.name.substring(0, 1).toUpperCase()
}}</span>
</v-avatar>
<span class="user-name">
{{
member.userId == $matrix.currentUserId
? $t("room_info.user_you", {
user: member.user ? member.user.displayName : member.name,
})
: $t("room_info.user", {
user: member.user ? member.user.displayName : member.name,
})
}}
</span>
<span v-if="isAdmin(member)" class="user-power">
{{ $t("room_info.user_admin") }}
</span>
<span v-else-if="isModerator(member)" class="user-power">
{{ $t("room_info.user_moderator") }}
</span>
</div>
</v-expansion-panel-header>
<v-expansion-panel-content>
<v-tabs>
<v-tab v-if="member.userId != $matrix.currentUserId">{{ $t("menu.actions") }}</v-tab>
<v-tab>{{$t('device_list.title')}}</v-tab>
<!-- TODO: other rooms in common
<v-tab v-if="member.userId != $matrix.currentUserId">{{ $t("menu.common_room") }}</v-tab>
-->
<v-tab-item v-if="member.userId != $matrix.currentUserId">
<div class="py-3">
<v-btn text v-if="member.userId != $matrix.currentUserId && !$matrix.isDirectRoomWith(room, member.userId)" class="start-private-chat clickable d-block text-none" @click="startPrivateChat(member.userId)">{{ $t("menu.start_private_chat") }}</v-btn>
<div v-if="canKickUser(member) || canBanUser(member)">
<div v-if="member.userId != $matrix.currentUserId" class="mt-2">{{ String.fromCharCode(160) }}</div>
<v-btn text v-if="member.userId != $matrix.currentUserId && canKickUser(member)" class="start-private-chat clickable d-block text-none" @click="kickUser(member)">{{ $t("menu.user_kick") }}</v-btn>
<v-btn text v-if="member.userId != $matrix.currentUserId && canBanUser(member)" class="start-private-chat clickable d-block text-none" @click="banUser(member)">{{ $t("menu.user_kick_and_ban") }}</v-btn>
</div>
<div v-if="member.userId != $matrix.currentUserId" class="mt-2">{{ String.fromCharCode(160) }}</div>
<v-btn text v-if="member.userId != $matrix.currentUserId && !isAdmin(member) && canMakeAdmin(member)" class="start-private-chat clickable d-block text-none" @click="makeAdmin(member)">{{ $t("menu.user_make_admin") }}</v-btn>
<v-btn text v-if="member.userId != $matrix.currentUserId && !isModerator(member) && !isAdmin(member) && canMakeModerator(member)" class="start-private-chat clickable d-block text-none" @click="makeModerator(member)">{{ $t("menu.user_make_moderator") }}</v-btn>
<v-btn text v-if="member.userId != $matrix.currentUserId && isModerator(member) && canRevokeModerator(member)" class="start-private-chat clickable d-block text-none" @click="revokeModerator(member)">{{ $t("menu.user_revoke_moderator") }}</v-btn>
</div>
</v-tab-item>
<v-tab-item>
<DeviceList :member="member" />
</v-tab-item>
<!-- <v-tab-item v-if="member.userId != $matrix.currentUserId">
{{ "TODO" }}
</v-tab-item> -->
</v-tabs>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
<div class="show-all p-2" @click="showAllMembers = !showAllMembers" v-if="members.length > SHOW_MEMBER_LIMIT">
{{ showAllMembers ? $t("room_info.hide_all") : $t("room_info.show_all") }}
</div>
</v-card>
<!-- EXPORT CHAT -->
@ -247,6 +269,12 @@
@close="showPurgeConfirmation = false"
/>
<MessageRetentionDialog
:show="showMessageRetentionDialog"
:room="room"
@close="showMessageRetentionDialog = false"
/>
<RoomExport :room="room" v-if="exporting" v-on:close="exporting = false" />
</div>
</template>
@ -254,6 +282,7 @@
<script>
import LeaveRoomDialog from "../components/LeaveRoomDialog";
import PurgeRoomDialog from "../components/PurgeRoomDialog";
import MessageRetentionDialog from "../components/MessageRetentionDialog";
import DeviceList from "../components/DeviceList";
import RoomExport from "../components/RoomExport";
import RoomAvatarPicker from "../components/RoomAvatarPicker";
@ -269,6 +298,7 @@ export default {
components: {
LeaveRoomDialog,
PurgeRoomDialog,
MessageRetentionDialog,
DeviceList,
RoomExport,
RoomAvatarPicker,
@ -282,7 +312,8 @@ export default {
showAllMembers: false,
showLeaveConfirmation: false,
showPurgeConfirmation: false,
expandedMembers: [],
showMessageRetentionDialog: false,
messageRetention: "",
buildVersion: "",
updatingJoinRule: false, // Flag if we are processing update curerntly
joinRules: [
@ -298,12 +329,13 @@ export default {
},
],
SHOW_MEMBER_LIMIT: 5,
exporting: false,
};
exporting: false
};
},
mounted() {
this.$matrix.on("Room.timeline", this.onEvent);
this.updateMembers();
this.updateMessageRetention();
this.user = this.$matrix.matrixClient.getUser(this.$matrix.currentUserId);
// Display build version
@ -382,6 +414,7 @@ export default {
handler() {
console.log("RoomInfo: Current room changed");
this.updateMembers();
this.updateMessageRetention();
},
},
},
@ -391,11 +424,17 @@ export default {
if (this.room && this.room.roomId == event.getRoomId()) {
// For this room
if (event.getType() == "m.room.member") {
this.updateMembers();
}
this.updateMembers();
} else if (event.getType() == "m.room.retention") {
this.updateMessageRetention(event);
}
}
},
updateMessageRetention(event) {
this.messageRetention = this.roomMessageRetentionDisplay(event);
},
updateMembers() {
if (this.room) {
const myUserId = this.$matrix.currentUserId;
@ -441,15 +480,6 @@ export default {
this.$navigation.push({ name: "Profile" }, 1);
},
toggleMemberExpanded(member) {
const index = this.expandedMembers.indexOf(member);
if (index > -1) {
this.expandedMembers.splice(index, 1);
} else {
this.expandedMembers.push(member);
}
},
/**
* Set room join rule.
* @param joinRule One of "invite" or "public". Currently always disables guest access.

View file

@ -90,7 +90,12 @@ export default {
return this.sortItemsOnName(this.$matrix.invites);
},
joinedRooms() {
return this.sortItemsOnName(this.$matrix.joinedRooms);
// show room with notification on top, followed by room decending order by active Timestamp
return [...this.$matrix.joinedRooms].sort((a, b) => {
if (this.notificationCount(a)) return -1;
if (this.notificationCount(b)) return 1;
return b.getLastActiveTimestamp() - a.getLastActiveTimestamp()
});
},
},
methods: {

View file

@ -15,7 +15,7 @@
</v-avatar>
<!-- SLOT FOR CONTENT -->
<slot></slot>
<div class="op-button" ref="opbutton" v-if="!event.isRedacted() && !$matrix.currentRoomIsReadOnlyForUser">
<div class="op-button" ref="opbutton" v-if="!event.isRedacted()">
<v-btn id="btn-more" icon @click.stop="showContextMenu($refs.opbutton)">
<v-icon>more_vert</v-icon>
</v-btn>

View file

@ -8,7 +8,7 @@
<v-btn id="btn-more" icon @click.stop="more" class="ma-0 pa-0">
<v-icon small> $vuetify.icons.addReaction </v-icon>
</v-btn>
<v-btn v-if="incoming" id="btn-reply" icon @click.stop="addReply" class="ma-0 pa-0">
<v-btn v-if="incoming && !readOnlyRoom" id="btn-reply" icon @click.stop="addReply" class="ma-0 pa-0">
<v-icon>reply</v-icon>
</v-btn>
<v-btn id="btn-edit" icon @click.stop="edit" class="ma-0 pa-0" v-if="isEditable">
@ -40,6 +40,12 @@ export default {
default: function () {
return []
}
},
readOnlyRoom: {
type: Boolean,
default: function () {
return false;
}
}
},
computed: {

View file

@ -14,7 +14,7 @@ export default {
editedRoomTopic: "",
isRoomTopicEditMode: false,
roomTopicErrorMessage: null,
}
};
},
mounted() {
this.$matrix.on("Room.timeline", this.roomInfoMixinOnEvent);
@ -61,7 +61,10 @@ export default {
publicRoomLink() {
if (this.room && this.roomJoinRule == "public") {
return this.$router.getRoomLink(
this.room.getCanonicalAlias(), this.room.roomId, this.room.name, utils.roomDisplayTypeToQueryParam(this.room, this.roomDisplayType)
this.room.getCanonicalAlias(),
this.room.roomId,
this.room.name,
utils.roomDisplayTypeToQueryParam(this.room, this.roomDisplayType)
);
}
return null;
@ -69,7 +72,7 @@ export default {
roomHistory() {
if (this.room) {
return this.room.shouldEncryptForInvitedMembers()
return this.room.shouldEncryptForInvitedMembers();
}
return false;
},
@ -92,13 +95,24 @@ export default {
*/
privateParty() {
if (this.isPrivate) {
const membersButMe = this.room.getMembers().filter(m => m.userId != this.$matrix.currentUserId);
const membersButMe = this.room.getMembers().filter((m) => m.userId != this.$matrix.currentUserId);
if (membersButMe.length == 1) {
return membersButMe[0];
}
}
return undefined;
},
canViewRetentionPolicy() {
return true;
},
/**
* Return true if we can set message retention policy in the room.
*/
canChangeRetentionPolicy() {
return this.userCanExportChat;
},
},
watch: {
room: {
@ -115,14 +129,48 @@ export default {
},
},
methods: {
/**
* Get a string describing current room retention setting.
* Can be "None", "1 week", "1 hour" etc...
*/
roomMessageRetentionDisplay(maybeEvent) {
const retention = this.roomMessageRetention(maybeEvent);
if (retention == 0) {
return this.$t("room_info.message_retention_none");
} else if (retention == 60 * 60 * 1000) {
return this.$t("room_info.message_retention_1_hour");
} else if (retention == 24 * 60 * 60 * 1000) {
return this.$t("room_info.message_retention_1_day");
} else if (retention == 7 * 24 * 60 * 60 * 1000) {
return this.$t("room_info.message_retention_1_week");
} else if (retention == 30 * 24 * 60 * 60 * 1000) {
return this.$t("room_info.message_retention_30_days");
} else if (retention == 365 * 24 * 60 * 60 * 1000) {
return this.$t("room_info.message_retention_365_days");
}
return this.$t("room_info.message_retention_other_seconds", { count: retention / 1000 });
},
roomMessageRetention(maybeEvent) {
const retentionEvent = maybeEvent || (this.room && this.room.currentState.getStateEvents("m.room.retention", ""));
if (retentionEvent) {
console.log("Retention event found", JSON.stringify(retentionEvent));
const maxLifetime = parseInt(retentionEvent.getContent().max_lifetime);
if (maxLifetime) {
return maxLifetime;
}
}
return 0;
},
onRoomNameClicked() {
if(this.userCanPurgeRoom) {
if (this.userCanPurgeRoom) {
this.isRoomNameEditMode = !this.isRoomNameEditMode;
this.editedRoomName = this.roomName;
}
},
updateRoomName() {
if(this.editedRoomName) {
if (this.editedRoomName) {
this.$matrix.matrixClient.setRoomName(this.room.roomId, this.editedRoomName);
this.isRoomNameEditMode = !this.isRoomNameEditMode;
} else {
@ -130,13 +178,13 @@ export default {
}
},
onRoomTopicClicked() {
if(this.userCanPurgeRoom) {
if (this.userCanPurgeRoom) {
this.isRoomTopicEditMode = !this.isRoomTopicEditMode;
this.editedRoomTopic = this.roomTopic;
}
},
updateRoomTopic() {
if(this.editedRoomTopic) {
if (this.editedRoomTopic) {
this.$matrix.matrixClient.setRoomTopic(this.room.roomId, this.editedRoomTopic);
this.isRoomTopicEditMode = !this.isRoomTopicEditMode;
} else {
@ -154,14 +202,8 @@ export default {
if (this.room) {
this.roomJoinRule = this.getRoomJoinRule();
const canChangeAccess =
this.room.currentState.mayClientSendStateEvent(
"m.room.join_rules",
this.$matrix.matrixClient
) &&
this.room.currentState.mayClientSendStateEvent(
"m.room.guest_access",
this.$matrix.matrixClient
);
this.room.currentState.mayClientSendStateEvent("m.room.join_rules", this.$matrix.matrixClient) &&
this.room.currentState.mayClientSendStateEvent("m.room.guest_access", this.$matrix.matrixClient);
this.userCanChangeJoinRule = canChangeAccess;
this.userCanPurgeRoom = canChangeAccess; //TODO - need different permissions here?
} else {
@ -175,10 +217,7 @@ export default {
if (event.getRoomId() !== this.roomId) {
return; // Not for this room
}
if (
event.getType() == "m.room.join_rules" ||
event.getType() == "m.room.guest_access"
) {
if (event.getType() == "m.room.join_rules" || event.getType() == "m.room.guest_access") {
this.updatePermissions();
}
},
@ -186,15 +225,9 @@ export default {
privatePartyAvatar(size) {
const other = this.privateParty;
if (other) {
return other.getAvatarUrl(
this.$matrix.matrixClient.getHomeserverUrl(),
size,
size,
"scale",
true
);
return other.getAvatarUrl(this.$matrix.matrixClient.getHomeserverUrl(), size, size, "scale", true);
}
return undefined;
},
},
}
};

View file

@ -1,17 +1,13 @@
import { isMobileOrTabletBrowser } from './utils'
// Installing your PWA is required for periodic syncs to work
const registerPeriodicBackgroundSync = async (registration) => {
// Check if periodicSync is supported
if ('periodicSync' in registration) {
// Request permission
const status = await navigator.permissions.query({
name: 'periodic-background-sync',
});
if (status.state === 'granted') {
console.log('PBS registered and granted')
console.log('Periodic background sync registered and granted')
try {
// Register the periodic background sync.
await registration.periodicSync.register('check-new-messages', {
// minInterval is one day
minInterval: 24 * 60 * 60 * 1000,
@ -19,11 +15,10 @@ const registerPeriodicBackgroundSync = async (registration) => {
console.log('Periodic background sync registered!');
console.log(registration.periodicSync.getTags())
// List registered periodic background sync tags.
const tags = await registration.periodicSync.getTags();
if (tags.length) {
tags.forEach((tag) => {
console.log('tag')
console.log('tag name')
console.log(tag)
});
}
@ -39,15 +34,15 @@ const registerPeriodicBackgroundSync = async (registration) => {
}
export function registerServiceWorker() {
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
const registration = await navigator.serviceWorker.register("/sw.js");
console.log('Service worker registered for scope', registration.scope);
if(isMobileOrTabletBrowser) {
if("serviceWorker" in navigator) {
navigator.serviceWorker.register("./sw.js")
.then(async registration => {
console.log('Service Worker registered with scope:', registration.scope);
await registerPeriodicBackgroundSync(registration);
}
});
})
.catch(error => {
console.error('Service Worker registration failed:', error);
});
} else {
console.log("No Service Worker support!");
}

View file

@ -924,17 +924,6 @@ class Util {
});
}
isMobileOrTabletBrowser() {
// Regular expression to match common mobile and tablet browser user agent strings
const mobileTabletPattern = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Tablet|Mobile|CriOS/i;
// Get the user agent string
const userAgent = navigator.userAgent;
// Check if the user agent matches the pattern for mobile or tablet browsers
return mobileTabletPattern.test(userAgent);
}
getMatrixBaseUrl(user, config) {
if (user) {
const domain = User.domainPart(user.user_id);