Support pinning thread messages (media)
This commit is contained in:
parent
d3ffc3d15b
commit
5e1223fc01
10 changed files with 152 additions and 27 deletions
|
|
@ -83,6 +83,10 @@
|
|||
.bubble.image-bubble {
|
||||
/* full bleed */
|
||||
padding: 0 0 0 0;
|
||||
border-radius: 0 !important;
|
||||
.v-image {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
}
|
||||
.quick-reaction-container {
|
||||
order: 6;
|
||||
|
|
@ -139,4 +143,55 @@
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Style file items (i.e. PDF files) */
|
||||
.thumbnail-item.file-item {
|
||||
border: 1px solid black;
|
||||
border-radius: 8px;
|
||||
padding: 15px 40px 15px 60px;
|
||||
align-items: start;
|
||||
position: relative;
|
||||
svg {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
&::after {
|
||||
content: " ";
|
||||
background: url("~@/assets/icons/ic_export.svg") no-repeat;
|
||||
background-position: 0 0;
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
margin: auto 0;
|
||||
}
|
||||
}
|
||||
|
||||
.swipeable-thumbnails-view {
|
||||
margin-left: -15px;
|
||||
margin-right: -15px;
|
||||
.thumbnail-item.file-item {
|
||||
margin: 15px;
|
||||
width: auto;
|
||||
}
|
||||
.indicator-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 12px;
|
||||
.indicator {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
margin: 0 2.5px;
|
||||
background: #D9D9D9;
|
||||
&.current {
|
||||
background: #1C1C31;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
4
src/assets/icons/ic_export.svg
Normal file
4
src/assets/icons/ic_export.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.55 3H7.25C7.71944 3 8.1 3.38056 8.1 3.85C8.1 4.28591 7.77187 4.64518 7.34913 4.69428L7.25 4.7H5.55C5.11409 4.7 4.75482 5.02813 4.70572 5.45087L4.7 5.55V17.45C4.7 17.8859 5.02813 18.2452 5.45087 18.2943L5.55 18.3H17.45C17.8859 18.3 18.2452 17.9719 18.2943 17.5491L18.3 17.45V15.75C18.3 15.2806 18.6806 14.9 19.15 14.9C19.5859 14.9 19.9452 15.2281 19.9943 15.6509L20 15.75V17.45C20 18.808 18.9384 19.9181 17.5998 19.9957L17.45 20H5.55C4.19197 20 3.08189 18.9384 3.00433 17.5998L3 17.45V5.55C3 4.19197 4.06158 3.08189 5.40017 3.00433L5.55 3ZM19.15 3L19.2188 3.00255L19.3206 3.0172L19.4153 3.04228L19.5097 3.07962L19.5926 3.1241L19.6742 3.18087L19.751 3.24896L19.8331 3.34399L19.894 3.43855L19.9204 3.49037L19.9491 3.5596L19.9695 3.62391L19.9941 3.74962L20 3.85V8.95C20 9.41944 19.6194 9.8 19.15 9.8C18.6806 9.8 18.3 9.41944 18.3 8.95V5.9019L12.951 11.251C12.6446 11.5575 12.1625 11.581 11.829 11.3218L11.749 11.251C11.4425 10.9446 11.419 10.4625 11.6782 10.129L11.749 10.049L17.0964 4.7H14.05C13.5806 4.7 13.2 4.31944 13.2 3.85C13.2 3.38056 13.5806 3 14.05 3H19.15Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -727,6 +727,11 @@ export default {
|
|||
// Filter out relations and redactions
|
||||
events = this.events.toReversed().filter((e) => !e.isRelation() && !e.isRedaction());
|
||||
|
||||
// If Channel, remove all redacted events as well.
|
||||
if (this.room && this.room.displayType == ROOM_TYPE_CHANNEL) {
|
||||
events = events.filter((e) => !e.isRedacted());
|
||||
}
|
||||
|
||||
// Add read marker, if it is not newer than the "latest" message we are going to display
|
||||
//
|
||||
let showReadMarker = false;
|
||||
|
|
@ -981,20 +986,22 @@ export default {
|
|||
|
||||
// Handle pinning
|
||||
//
|
||||
const pinnedEvents = this.$matrix.getPinnedEvents(this.room);
|
||||
console.log("Pinned events in room", JSON.stringify(pinnedEvents));
|
||||
events.forEach((e) => {
|
||||
Vue.set(e, "isPinned", pinnedEvents.includes(e.getId()));
|
||||
});
|
||||
updated = updated.sort((e1, e2) => {
|
||||
if (!e1.isPinned && !e2.isPinned) return 0;
|
||||
else if (e1.isPinned && !e2.isPinned) return this.reverseOrder ? 1 : -1;
|
||||
else if (e2.isPinned && !e1.isPinned) return this.reverseOrder ? -1 : 1;
|
||||
else {
|
||||
// Look at order in "pinned" value in the m.room.pinned_events event!
|
||||
return pinnedEvents.indexOf(e1.getId()) < pinnedEvents.indexOf(e2.getId()) ? (this.reverseOrder ? 1 : -1) : (this.reverseOrder ? -1 : 1)
|
||||
}
|
||||
});
|
||||
if (this.room) {
|
||||
const pinnedEvents = this.$matrix.getPinnedEvents(this.room);
|
||||
updated.forEach((e) => {
|
||||
Vue.set(e, "isPinned", pinnedEvents.includes(e.threadParent ? e.threadParent.getId() : e.getId()));
|
||||
});
|
||||
|
||||
updated = updated.sort((e1, e2) => {
|
||||
if (!e1.isPinned && !e2.isPinned) return 0;
|
||||
else if (e1.isPinned && !e2.isPinned) return this.reverseOrder ? 1 : -1;
|
||||
else if (e2.isPinned && !e1.isPinned) return this.reverseOrder ? -1 : 1;
|
||||
else {
|
||||
// Look at order in "pinned" value in the m.room.pinned_events event!
|
||||
return pinnedEvents.indexOf(e1.getId()) < pinnedEvents.indexOf(e2.getId()) ? (this.reverseOrder ? 1 : -1) : (this.reverseOrder ? -1 : 1)
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!onlyIfLengthChanges || updated.length != this.events.length) {
|
||||
this.events = updated; // Changed
|
||||
}
|
||||
|
|
@ -1781,11 +1788,13 @@ export default {
|
|||
},
|
||||
|
||||
pin(event) {
|
||||
this.$matrix.setEventPinned(this.room, event, true);
|
||||
const eventToPin = event.parentThread ? event.parentThread : event;
|
||||
this.$matrix.setEventPinned(this.room, eventToPin, true);
|
||||
},
|
||||
|
||||
unpin(event) {
|
||||
this.$matrix.setEventPinned(this.room, event, false);
|
||||
const eventToUnpin = event.parentThread ? event.parentThread : event;
|
||||
this.$matrix.setEventPinned(this.room, eventToUnpin, false);
|
||||
},
|
||||
|
||||
cancelEditReply() {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
<span ref="messageInOutRef" class="content">
|
||||
<slot></slot>
|
||||
</span>
|
||||
<div class="pin-icon" v-if="event.isPinned"><v-icon>$vuetify.icons.ic_pin_filled</v-icon></div>
|
||||
<div class="pin-icon" v-if="isPinned"><v-icon>$vuetify.icons.ic_pin_filled</v-icon></div>
|
||||
<div class="op-button" ref="opbutton" v-if="!event.isRedacted() && room.displayType != ROOM_TYPE_CHANNEL">
|
||||
<v-btn id="btn-more" icon @click.stop="showContextMenu($refs.opbutton)">
|
||||
<v-icon>more_vert</v-icon>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@
|
|||
</div>
|
||||
|
||||
<div class="message">
|
||||
<v-container v-if="!event.isRedacted()" fluid class="imageCollection">
|
||||
<SwipeableThumbnailsView :items="items" v-if="!event.isRedacted() && room.displayType == ROOM_TYPE_CHANNEL" />
|
||||
<v-container v-else-if="!event.isRedacted()" fluid class="imageCollection">
|
||||
<v-row wrap>
|
||||
<v-col v-for="({ size, item }) in layoutedItems()" :key="item.event.getId()" :cols="size">
|
||||
<ThumbnailView :item="item" :previewOnly="true" v-on:itemclick="onItemClick($event)" />
|
||||
|
|
@ -38,16 +39,18 @@
|
|||
<script>
|
||||
import MessageIncoming from "./MessageIncoming.vue";
|
||||
import messageMixin from "./messageMixin";
|
||||
import util from "../../plugins/utils";
|
||||
import util, { ROOM_TYPE_CHANNEL } from "../../plugins/utils";
|
||||
import GalleryItemsView from '../file_mode/GalleryItemsView.vue';
|
||||
import ThumbnailView from '../file_mode/ThumbnailView.vue';
|
||||
import SwipeableThumbnailsView from "./channel/SwipeableThumbnailsView.vue";
|
||||
|
||||
export default {
|
||||
extends: MessageIncoming,
|
||||
components: { MessageIncoming, GalleryItemsView, ThumbnailView },
|
||||
components: { MessageIncoming, GalleryItemsView, ThumbnailView, SwipeableThumbnailsView },
|
||||
mixins: [messageMixin],
|
||||
data() {
|
||||
return {
|
||||
ROOM_TYPE_CHANNEL: ROOM_TYPE_CHANNEL,
|
||||
items: [],
|
||||
showItem: null,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
<v-icon>more_vert</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<div class="pin-icon" v-if="event.isPinned"><v-icon>$vuetify.icons.ic_pin_filled</v-icon></div>
|
||||
<div class="pin-icon" v-if="isPinned"><v-icon>$vuetify.icons.ic_pin_filled</v-icon></div>
|
||||
|
||||
<!-- SLOT FOR CONTENT -->
|
||||
<span ref="messageInOutRef" class="content">
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@
|
|||
|
||||
|
||||
<div class="message">
|
||||
<v-container v-if="!event.isRedacted()" fluid class="imageCollection">
|
||||
<SwipeableThumbnailsView :items="items" v-if="!event.isRedacted() && room.displayType == ROOM_TYPE_CHANNEL" />
|
||||
<v-container v-else-if="!event.isRedacted()" fluid class="imageCollection">
|
||||
<v-row wrap>
|
||||
<v-col v-for="({ size, item }) in layoutedItems()" :key="item.event.getId()" :cols="size">
|
||||
<ThumbnailView :item="item" :previewOnly="true" v-on:itemclick="onItemClick($event)" />
|
||||
|
|
@ -39,16 +40,18 @@
|
|||
<script>
|
||||
import MessageOutgoing from "./MessageOutgoing.vue";
|
||||
import messageMixin from "./messageMixin";
|
||||
import util from "../../plugins/utils";
|
||||
import util, { ROOM_TYPE_CHANNEL } from "../../plugins/utils";
|
||||
import GalleryItemsView from '../file_mode/GalleryItemsView.vue';
|
||||
import ThumbnailView from '../file_mode/ThumbnailView.vue';
|
||||
import SwipeableThumbnailsView from "./channel/SwipeableThumbnailsView.vue";
|
||||
|
||||
export default {
|
||||
extends: MessageOutgoing,
|
||||
components: { MessageOutgoing, GalleryItemsView, ThumbnailView },
|
||||
components: { MessageOutgoing, GalleryItemsView, ThumbnailView, SwipeableThumbnailsView },
|
||||
mixins: [messageMixin],
|
||||
data() {
|
||||
return {
|
||||
ROOM_TYPE_CHANNEL: ROOM_TYPE_CHANNEL,
|
||||
items: [],
|
||||
showItem: null,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,12 @@
|
|||
<v-list-item-title>{{ $t("menu.unpin") }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item key="redact" v-if="isRedactable" @click.stop="redact">
|
||||
<v-list-item-icon><v-icon color="#222222">delete_outline</v-icon></v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ $t("menu.delete") }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
41
src/components/messages/channel/SwipeableThumbnailsView.vue
Normal file
41
src/components/messages/channel/SwipeableThumbnailsView.vue
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<template>
|
||||
<div class="swipeable-thumbnails-view">
|
||||
<v-responsive :aspect-ratio="16 / 9" class="ma-0 pa-0">
|
||||
<v-carousel height="100%" hide-delimiters show-arrows-on-hover v-model="currentIndex">
|
||||
<v-carousel-item v-for="(item,index) in items" :key="item.event.getId()">
|
||||
<ThumbnailView :item="items[index]" :previewOnly="true" />
|
||||
</v-carousel-item>
|
||||
</v-carousel>
|
||||
</v-responsive>
|
||||
<div class="indicator-container">
|
||||
<div v-for="(item,index) in items" :key="index" :class="{'indicator': true, 'current': index == currentIndex}" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import messageMixin from "../../messages/messageMixin";
|
||||
import ThumbnailView from '../../file_mode/ThumbnailView.vue';
|
||||
|
||||
export default {
|
||||
mixins: [messageMixin],
|
||||
components: { ThumbnailView },
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: function () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentIndex: 0,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "@/assets/css/chat.scss";
|
||||
</style>
|
||||
|
|
@ -109,7 +109,7 @@ export default {
|
|||
* Don't show sender and time if the next event is within 2 minutes and also from us (= back to back messages)
|
||||
*/
|
||||
showSenderAndTime() {
|
||||
if (!this.event.isPinned && this.nextEvent && this.nextEvent.getSender() == this.event.getSender()) {
|
||||
if (!this.isPinned && this.nextEvent && this.nextEvent.getSender() == this.event.getSender()) {
|
||||
const ts1 = this.nextEvent.event.origin_server_ts;
|
||||
const ts2 = this.event.event.origin_server_ts;
|
||||
return ts1 - ts2 < 2 * 60 * 1000; // less than 2 minutes
|
||||
|
|
@ -180,15 +180,19 @@ export default {
|
|||
return this.event.getContent().body;
|
||||
},
|
||||
|
||||
isPinned() {
|
||||
return this.event.parentThread ? this.event.parentThread.isPinned : this.event.isPinned;
|
||||
},
|
||||
|
||||
/**
|
||||
* Classes to set for the message. Currently only for "messageIn"
|
||||
*/
|
||||
|
||||
messageClasses() {
|
||||
if (this.incoming) {
|
||||
return { messageIn: true, "from-admin": this.senderIsAdminOrModerator(this.event), "pinned": this.event.isPinned };
|
||||
return { messageIn: true, "from-admin": this.senderIsAdminOrModerator(this.event), "pinned": this.isPinned };
|
||||
} else {
|
||||
return { messageOut: true, "pinned": this.event.isPinned };
|
||||
return { messageOut: true, "pinned": this.isPinned };
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue