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

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