merge master
This commit is contained in:
commit
8ca2d8ec07
28 changed files with 608 additions and 95 deletions
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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: "",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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, "", ""),
|
||||
|
|
|
|||
19
src/components/NoHistoryRoomWelcomeHeader.vue
Normal file
19
src/components/NoHistoryRoomWelcomeHeader.vue
Normal 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>
|
||||
|
|
@ -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">
|
||||
<< {{ $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() {
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
10
src/components/logoMixin.js
Normal file
10
src/components/logoMixin.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export default {
|
||||
computed: {
|
||||
logotype() {
|
||||
if (this.$config.logo) {
|
||||
return this.$config.logo;
|
||||
}
|
||||
return require("@/assets/logo.svg");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
96
src/components/messages/SeenBy.vue
Normal file
96
src/components/messages/SeenBy.vue
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue