Merge branch 'dev'

This commit is contained in:
N-Pex 2024-08-23 09:45:13 +02:00
commit cbad68ecec
21 changed files with 194 additions and 50 deletions

View file

@ -54,7 +54,6 @@ The app loads runtime configutation from the server at "./config.json" and merge
* **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".
* **maxSizeAutoDownloads** - Attachments smaller than this will be auto downloaded. Default is 10Mb.
### 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)

View file

@ -1,6 +1,6 @@
{
"name": "keanuapp-weblite",
"version": "0.1.39",
"version": "0.1.40",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",

View file

@ -1,6 +1,6 @@
{
"name": "keanuapp-weblite",
"version": "0.1.38",
"version": "0.1.39",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",

View file

@ -25,6 +25,7 @@
type="list-item-avatar-two-line, divider, list-item-three-line, card-heading"
v-if="showLoadingScreen"
></v-skeleton-loader>
<unsupported-browser-alert />
</v-main>
</v-app>
</template>
@ -34,10 +35,14 @@ import stickers from "./plugins/stickers";
import { registerServiceWorker, notificationCount, windowNotificationPermission } from "./plugins/notificationAndServiceWorker.js"
import logoMixin from "./components/logoMixin";
import { mapState } from 'vuex'
import UnsupportedBrowserAlert from "./components/UnsupportedBrowserAlert.vue";
export default {
name: "App",
mixins: [logoMixin],
components: {
UnsupportedBrowserAlert
},
data() {
return {
loading: true,

View file

@ -19,7 +19,9 @@
"days": "1 day ago | {n} days ago"
},
"close": "close",
"notify": "Notify"
"notify": "Notify",
"different_browser_title": "Try different browser",
"different_browser_content": "Some features may break. Copy and open link in a different browser."
},
"menu": {
"start_private_chat": "Direct Message with this user",
@ -142,6 +144,7 @@
"join_channel": "All set! Invite people to join you: {link}",
"info_retention": "🕓 Messages sent within {time} are viewable by anyone with the link.",
"info_retention_user": "🕓 Messages older than {time} will be deleted from the history.",
"info_auto_join": "Welcome to {room}.\nYou are joining as {you}.",
"change": "Change"
},
"new_room": {

View file

@ -174,7 +174,8 @@
"message_retention_1_day": "1 päivä",
"message_retention_8_hours": "8 tuntia",
"message_retention_1_hour": "1 tunti",
"read_only_room": "Lue Ainoastaan"
"read_only_room": "Lue Ainoastaan",
"moderation": "Moderointi"
},
"power_level": {
"restricted": "rajoitettu",

View file

@ -18,7 +18,8 @@
"delete": "Supprimer",
"done": "Terminé",
"user_kick_and_ban": "Exclure",
"user_make_admin": "Rendre administrateur"
"user_make_admin": "Rendre administrateur",
"direct_chat": "Discussion directe"
},
"language_display_name": "français",
"message": {
@ -222,7 +223,8 @@
"message_retention_1_day": "1 jour",
"message_retention_8_hours": "8 heures",
"message_retention_1_hour": "1 heure",
"read_only_room": "Lecture seule"
"read_only_room": "Lecture seule",
"moderation": "Modération"
},
"room_info_sheet": {
"this_room": "Ce salon",

View file

@ -125,7 +125,8 @@
"send_verification": "Invia email di verifica",
"invalid_message": "Nome utente o password non validi",
"accept_terms": "Accetto",
"resend_verification": "Rispedisci email di verifica"
"resend_verification": "Rispedisci email di verifica",
"token_not_valid": "Token non valido"
},
"profile": {
"title": "Il mio profilo",
@ -218,7 +219,8 @@
"message_retention_1_day": "1 giorno",
"message_retention_8_hours": "8 ore",
"message_retention_1_hour": "1 ora",
"read_only_room": "Sola lettura"
"read_only_room": "Sola lettura",
"moderation": "Moderazione"
},
"voice_recorder": {
"failed_to_record": "Impossibile registrare laudio",

View file

@ -155,7 +155,8 @@
"change": "Mudança",
"join_channel": "Tudo pronto! Convide pessoas para se juntarem a você: {link}",
"info_retention": "🕓 As mensagens enviadas dentro de {time} podem ser visualizadas por qualquer pessoa com o link.",
"info_retention_user": "As mensagens mais antigas que {time} serão excluídas do histórico."
"info_retention_user": "As mensagens mais antigas que {time} serão excluídas do histórico.",
"info_auto_join": "Bem-vindo à {room}.\nVocê está entrando como {you}."
},
"new_room": {
"new_room": "Nova sala",

View file

@ -268,7 +268,8 @@
"info_permissions": "Вы можете в любой момент изменить \"разрешение на присоединение\" в настройках комнаты.",
"info": "Добро пожаловать! Вот несколько вещей, которые нужно знать о вашей комнате:",
"direct_private_chat": "Личное сообщение",
"info_retention_user": "🕓 Сообщения старше {time} будут удалены из истории."
"info_retention_user": "🕓 Сообщения старше {time} будут удалены из истории.",
"info_auto_join": "Добро пожаловать в {room}.\nВы присоединяетесь как {you}."
},
"device_list": {
"blocked": "Заблокированный",

View file

@ -49,7 +49,8 @@
"message_retention_8_hours": "8 小时",
"message_retention_1_hour": "1 小时",
"message_retention_2_week": "2周",
"message_retention_1_week": "1周"
"message_retention_1_week": "1周",
"moderation": "调节面板"
},
"leave": {
"leave": "离开",

View file

@ -37,7 +37,6 @@
<div ref="messageOperationsStrut" class="message-operations-strut">
<message-operations ref="messageOperations" :style="opStyle" :emojis="recentEmojis" v-on:close="
showContextMenu = false;
showContextMenuAnchor = null;
" v-if="showMessageOperations" v-on:addreaction="addReaction" v-on:addquickreaction="addQuickReaction"
v-on:addreply="addReply(selectedEvent)" v-on:edit="edit(selectedEvent)" v-on:redact="redact(selectedEvent)"
v-on:download="download(selectedEvent)" v-on:more="
@ -55,7 +54,7 @@
<component :is="roomWelcomeHeader" v-on:close="closeRoomWelcomeHeader"></component>
<!-- If we have a retention timer, it means we have active message retention. Show header. -->
<WelcomeHeaderChannelUser v-if="retentionTimer && !roomWelcomeHeader" />
<WelcomeHeaderChannelUser v-if="retentionTimer && !roomWelcomeHeader && newlyJoinedRoom" />
<div v-for="(event, index) in filteredEvents" :key="event.getId()" :eventId="event.getId()">
<!-- DAY Marker, shown for every new day in the timeline -->
@ -479,6 +478,7 @@ export default {
/** If we just created this room, show a small welcome header with info */
hideRoomWelcomeHeader: false,
newlyJoinedRoom: false,
/** An array of recent emojis. Used in the "message operations" popup. */
recentEmojis: [],
@ -831,6 +831,7 @@ export default {
this.typingMembers = [];
this.initialLoadDone = false;
this.hideRoomWelcomeHeader = false;
this.newlyJoinedRoom = false;
// Stop RR timer
this.stopRRTimer();
@ -862,8 +863,8 @@ export default {
this.onRoomJoined(this.readMarker);
}
},
showMessageOperations() {
if (this.showMessageOperations) {
showMessageOperations(show) {
if (show) {
this.$nextTick(() => {
// Calculate where to show the context menu.
//
@ -958,6 +959,16 @@ export default {
},
onRoomJoined(initialEventId) {
// If our own join event is less than a minute old, consider this a "newly joined" room.
//
// Previously tried to look at initialEventId, but it seems like "this.room.getEventReadUpTo(this.$matrix.currentUserId, false)"
// always returns an event id? Strange. I would expect it to be null on a fresh room.
//
const joinEvent = this.room && this.room.currentState.getStateEvents("m.room.member", this.$matrix.currentUserId);
if (joinEvent) {
this.newlyJoinedRoom = joinEvent.getLocalAge() < 1 * 60000 /* 1 minute */;
}
// Listen to events
this.$matrix.on("Room.timeline", this.onEvent);
this.$matrix.on("RoomMember.typing", this.onUserTyping);
@ -1044,7 +1055,7 @@ export default {
this.$navigation.push(
{
name: "Join",
params: { roomId: util.sanitizeRoomId(this.roomAliasOrId) },
params: { roomId: util.sanitizeRoomId(this.roomAliasOrId), join: this.$route.params.join },
},
0
);
@ -1740,10 +1751,17 @@ export default {
showContextMenuForEvent(e) {
const event = e.event;
this.selectedEvent = event;
this.updateRecentEmojis();
this.showContextMenu = !this.showContextMenu;
this.showContextMenuAnchor = e.anchor;
if (this.selectedEvent == event) {
this.showContextMenu = !this.showContextMenu;
} else {
this.showContextMenu = false;
this.$nextTick(() => {
this.selectedEvent = event;
this.updateRecentEmojis();
this.showContextMenu = true;
this.showContextMenuAnchor = e.anchor;
})
}
},
showAvatarMenuForEvent(e) {
@ -1761,6 +1779,7 @@ export default {
if (this.showContextMenu) {
this.showContextMenu = false;
this.showContextMenuAnchor = null;
this.selectedEvent = null;
e.preventDefault();
}
},

View file

@ -105,6 +105,9 @@
<input id="room-avatar-picker" ref="avatar" type="file" name="avatar" @change="handlePickedAvatar($event)"
accept="image/*" class="d-none" />
<input id="user-avatar-picker" ref="useravatar" type="file" name="user-avatar" @change="handlePickedUserAvatar($event)" accept="image/*" class="d-none" />
<v-dialog v-model="enterRoomDialog" :width="$vuetify.breakpoint.smAndUp ? '50%' : '90%'">
<v-card>
<v-container v-if="canEditProfile" class="pa-10">
@ -112,9 +115,11 @@
<v-col class="py-0">
<div class="text-start font-weight-bold">{{ $t("join.choose_name") }}</div>
<v-select ref="avatar" :items="availableAvatars" cache-items outlined dense @change="selectAvatar"
:value="availableAvatars[0]" single-line autofocus>
:value="selectedProfile">
<template v-slot:selection>
<v-text-field background-color="transparent" solo flat hide-details @click.native.stop="{}"
<v-text-field background-color="transparent" solo flat hide-details
@click.native.stop="(event) => event.target.focus()"
@focus="$event.target.select()"
v-model="selectedProfile.name"></v-text-field>
</template>
<template v-slot:item="data">
@ -126,7 +131,7 @@
</v-select>
</v-col>
<v-col cols="2" class="py-0">
<v-avatar @click="showAvatarPickerList">
<v-avatar @click="showUserAvatarPicker">
<v-img v-if="selectedProfile" :src="selectedProfile.image" />
</v-avatar>
</v-col>
@ -586,6 +591,21 @@ export default {
showAvatarPickerList() {
this.$refs.avatar.$refs.input.click();
},
/**
* Show picker to select user avatar file
*/
showUserAvatarPicker() {
if (this.step == steps.INITIAL) {
this.$refs.useravatar.click();
}
},
handlePickedUserAvatar(event) {
util.loadAvatarFromFile(event, (image) => {
this.selectedProfile.image = image;
});
},
}
};
</script>

View file

@ -18,7 +18,7 @@
</div>
<hr class="my-10 join-line" />
<div class="font-weight-black mb-4" v-if="!currentUser">Choose a name to use.</div>
<div class="font-weight-black mb-4" v-if="!currentUser">{{ $t("join.choose_name") }}</div>
<v-row v-if="canEditProfile">
<v-col cols="10" sm="7" class="py-0">
@ -29,7 +29,7 @@
outlined
dense
@change="selectAvatar"
:value="availableAvatars[0]"
:value="selectedProfile"
single-line
autofocus
>
@ -39,10 +39,8 @@
solo
flat
hide-details
@click.native.stop="
{
}
"
@click.native.stop="(event) => event.target.focus()"
@focus="$event.target.select()"
v-model="selectedProfile.name"
></v-text-field>
</template>
@ -308,7 +306,14 @@ export default {
return roomName ? roomName : "";
},
getRoomInfo() {
if (this.roomId.startsWith("#")) {
if (this.$route.params.join) {
// Auto-join room
this.waitingForRoomCreation = true;
this.$nextTick(() => {
this.handleJoin();
});
}
else if (this.roomId.startsWith("#")) {
this.$matrix
.getPublicRoomInfo(this.roomId)
.then((room) => {

View file

@ -0,0 +1,53 @@
<template>
<v-dialog
class="ma-0 pa-0"
v-model="isUnSupportedBrowser"
persistent
:width="$vuetify.breakpoint.smAndUp ? '50%' : '90%'"
>
<div class="dialog-content text-center">
<h2 class="dialog-title">{{ $t("global.different_browser_title") }}</h2>
<div class="dialog-text">{{ $t("global.different_browser_content") }}</div>
<v-card-actions class="pb-0">
<v-spacer></v-spacer>
<v-btn
:color="locationUrlCopied ? '#DEE6FF' : 'black'"
depressed
@click.stop="copyRoomLink1"
:class="{'filled-button' : true, 'link-copied-in-place' : locationUrlCopied}"
>{{ $t(`room_info.${locationUrlCopied ? 'link_copied' : 'copy_link'}`) }}</v-btn
>
</v-card-actions>
</div>
</v-dialog>
</template>
<script>
const UNSUPPORTED_USER_AGENT = [
'MicroMessenger' // WeChat
]
export default {
data () {
return {
locationUrlCopied: false,
locationUrl: window.location.href
}
},
computed: {
isUnSupportedBrowser() {
return UNSUPPORTED_USER_AGENT.some((userAgent) => window.navigator.userAgent.includes(userAgent));
}
},
methods: {
copyRoomLink1() {
if(this.locationUrlCopied) return
navigator.clipboard.writeText(this.locationUrl)
this.locationUrlCopied = true;
setInterval(() => {
this.locationUrlCopied = false;
}, 3000);
}
}
}
</script>

View file

@ -1,4 +1,4 @@
import utils from "../plugins/utils";
import utils, { ROOM_TYPE_CHANNEL } from "../plugins/utils";
import roomTypeMixin from "./roomTypeMixin";
export default {
@ -96,7 +96,8 @@ export default {
this.room.getCanonicalAlias(),
this.room.roomId,
this.room.name,
utils.roomDisplayTypeToQueryParam(this.room, this.roomDisplayType)
utils.roomDisplayTypeToQueryParam(this.room, this.roomDisplayType),
this.roomDisplayType == ROOM_TYPE_CHANNEL /* Auto join for channels */
);
}
return null;

View file

@ -20,12 +20,8 @@
<template v-slot:time>
<b>{{ messageRetentionDisplay }}</b>
</template>
</i18n>
</div>
<div class="mt-2" v-if="roomMessageRetention() > 0">
<a href="#" text @click.prevent="showMessageRetentionDialog = true">
{{ $t("room_welcome.change") }}
</a>
</i18n>&nbsp;
<a href="#" text @click.prevent="showMessageRetentionDialog = true" style="white-space: pre-line;">{{ $t("room_welcome.change") }}</a>
</div>
<div class="text-end">
<v-btn id="btn-got-it" text @click.stop="$emit('close')" class="text-transform-0">

View file

@ -1,5 +1,11 @@
<template>
<div class="created-room-welcome-header">
<div style="white-space: pre-line;">
{{ $t('room_welcome.info_auto_join',{room: room ? room.name : "",you: $matrix.currentUserDisplayName}) }}
<a href="#" text @click.prevent="viewProfile">
{{ $t("room_welcome.change") }}
</a>
</div>
<div class="mt-2" v-if="roomMessageRetention() > 0">
<i18n path="room_welcome.info_retention_user" tag="span">
<template v-slot:time>
@ -25,6 +31,9 @@ export default {
}
},
methods: {
viewProfile() {
this.$navigation.push({ name: "Profile" }, 1);
},
onMessageRetention(ignoredretention) {
this.updateMessageRetention();
},

View file

@ -920,7 +920,10 @@ class Util {
const link = document.createElement("a");
link.href = url;
link.target = "_blank";
link.download = event.getContent().body || this.$t("fallbacks.download_name");
if (!this.isFileTypePDF(event)) {
// PDFs are shown inline, not downloaded
link.download = event.getContent().body || this.$t("fallbacks.download_name");
}
document.body.appendChild(link);
link.click();
setTimeout(function () {

View file

@ -20,13 +20,13 @@ const routes = [
component: Home
},
{
path: '/room/:roomId?',
path: '/room/:join(join/)?:roomId?',
name: 'Chat',
component: Chat,
meta: {
includeRoom: true,
includeFavicon: true
}
},
},
{
path: '/info',
@ -73,7 +73,7 @@ const routes = [
props: true
},
{
path: '/join/:roomId?',
path: '/join/:join(join/)?:roomId?',
name: 'Join',
component: Join
},
@ -170,7 +170,7 @@ router.beforeEach((to, from, next) => {
}
});
router.getRoomLink = function (alias, roomId, roomName, mode) {
router.getRoomLink = function (alias, roomId, roomName, mode, autojoin) {
let params = {};
if ((!alias || roomName.replace(/\s/g, "").toLowerCase() !== util.getRoomNameFromAlias(alias)) && roomName) {
// There is no longer a correlation between alias and room name, probably because room name has
@ -181,13 +181,14 @@ router.getRoomLink = function (alias, roomId, roomName, mode) {
// Optional mode given, append as "m" query param
params["m"] = mode;
}
const autoJoinSegment = autojoin ? "join/" : "";
if (Object.entries(params).length > 0) {
const queryString = Object.entries(params)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&')
return window.location.origin + window.location.pathname + "?" + queryString + "#/room/" + encodeURIComponent(util.sanitizeRoomId(alias || roomId));
return window.location.origin + window.location.pathname + "?" + queryString + "#/room/" + autoJoinSegment + encodeURIComponent(util.sanitizeRoomId(alias || roomId));
}
return window.location.origin + window.location.pathname + "#/room/" + encodeURIComponent(util.sanitizeRoomId(alias || roomId));
return window.location.origin + window.location.pathname + "#/room/" + autoJoinSegment + encodeURIComponent(util.sanitizeRoomId(alias || roomId));
}
router.getDMLink = function (user, config) {

View file

@ -406,6 +406,17 @@ export default {
}
break;
case "m.room.join_rules":
{
const room = this.matrixClient.getRoom(event.getRoomId());
if (room && room.getJoinRule() == "private" && room.selfMembership == "invite") {
// We have an invite to a room that's now "private"? This is most probably a deleted DM room.
// Reject the invite, i.e. call "leave" on it.
this.matrixClient.leave(room.roomId);
}
}
break;
case STATE_EVENT_ROOM_DELETED:
{
const room = this.matrixClient.getRoom(event.getRoomId());
@ -494,7 +505,7 @@ export default {
// each time!
var updatedRooms = this.matrixClient.getVisibleRooms();
updatedRooms = updatedRooms.filter((room) => {
return room.selfMembership && (room.selfMembership == "invite" || room.selfMembership == "join");
return room.selfMembership && (room.selfMembership == "invite" || room.selfMembership == "join") && room.currentState.getStateEvents(STATE_EVENT_ROOM_DELETED).length == 0;
});
updatedRooms.forEach((room) => {
if (!room.avatar) {
@ -580,7 +591,7 @@ export default {
},
leaveRoom(roomId) {
return this.matrixClient.leave(roomId, undefined).then(() => {
return this.matrixClient.leave(roomId).then(() => {
this.$store.commit("setCurrentRoomId", null);
this.rooms = this.rooms.filter((room) => {
room.roomId != roomId;
@ -805,7 +816,7 @@ export default {
//console.log("Purge: set invite only");
statusCallback(this.$t("room.purge_set_room_state"));
withRetry(() => this.matrixClient.sendStateEvent(roomId, "m.room.join_rules", { join_rule: "invite" }, ""))
withRetry(() => this.matrixClient.sendStateEvent(roomId, "m.room.join_rules", { join_rule: "private" }, ""))
.then(() => {
//console.log("Purge: forbid guest access");
return withRetry(() => this.matrixClient.sendStateEvent(
@ -887,6 +898,8 @@ export default {
var invited = room.getMembersWithMembership("invite");
var allMembers = joined.concat(invited);
const me = allMembers.find((m) => m.userId == self.currentUserId);
const kickFirstMember = (members) => {
//console.log(`Kicking ${members.length} members`);
statusCallback(
@ -904,7 +917,16 @@ export default {
} else {
// Slight pause to avoid rate limiting.
return sleep(0.1)
.then(() => withRetry(() => this.matrixClient.kick(roomId, member.userId, "Room Deleted")))
.then(() => withRetry(() => {
if (member.membership == "invite" && me && me.powerLevel <= member.powerLevel) {
// The user is invited, but we can't kick them because of power levels.
// Send a new invite with reason set to "Room Deleted".
// The client will be sent stripped room state, and can from that see the
// join_rule of "private". It will then "leave", i.e. reject the invite.
return this.matrixClient.invite(roomId, member.userId, "Room Deleted");
}
return this.matrixClient.kick(roomId, member.userId, "Room Deleted")
}))
.then(() => kickFirstMember(members.slice(1)));
}
};