Timeline window and initial back paging
This commit is contained in:
parent
854a5ec770
commit
6c563b1e51
4 changed files with 178 additions and 83 deletions
|
|
@ -128,7 +128,7 @@ $chat-text-size: 0.7pt;
|
|||
}
|
||||
}
|
||||
|
||||
.sender {
|
||||
.sender, .status {
|
||||
font-family: 'Titillium Web', sans-serif;
|
||||
font-weight: 300;
|
||||
font-size: 15 * $chat-text-size;
|
||||
|
|
@ -151,3 +151,12 @@ $chat-text-size: 0.7pt;
|
|||
text-align: center;
|
||||
color: #1c242a;
|
||||
}
|
||||
|
||||
.statusEvent {
|
||||
font-family: 'Titillium Web', sans-serif;
|
||||
font-weight: 300;
|
||||
font-size: 15 * $chat-text-size;
|
||||
color: #1c242a;
|
||||
text-align: center;
|
||||
margin: 20px;
|
||||
}
|
||||
|
|
@ -4,8 +4,9 @@
|
|||
class="chat-content flex-grow-1 flex-shrink-1"
|
||||
ref="chatContainer"
|
||||
style="overflow-x: hidden; overflow-y: auto"
|
||||
v-on:scroll="onScroll"
|
||||
>
|
||||
<div v-for="(event, index) in events" :key="index">
|
||||
<div v-for="event in events" :key="event.eventId">
|
||||
<!-- Contact joined the chat -->
|
||||
<div
|
||||
class="messageJoin"
|
||||
|
|
@ -44,12 +45,11 @@
|
|||
|
||||
<div
|
||||
v-else-if="
|
||||
event.getSender() != myUserId &&
|
||||
event.getType() == 'm.room.message'
|
||||
event.getSender() != myUserId && event.getType() == 'm.room.message'
|
||||
"
|
||||
>
|
||||
<div class="messageIn">
|
||||
<div class="sender">{{ event.getSender() }}</div>
|
||||
<div class="sender">{{ messageEventDisplayName(event) }}</div>
|
||||
<div class="bubble">
|
||||
<div class="message">{{ event.getContent().body }}</div>
|
||||
</div>
|
||||
|
|
@ -64,11 +64,30 @@
|
|||
<div class="bubble">
|
||||
<div class="message">{{ event.getContent().body }}</div>
|
||||
</div>
|
||||
<div class="status">{{ event.status }}</div>
|
||||
</div>
|
||||
<div class="time">
|
||||
{{ formatTime(event.event.origin_server_ts) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ROOM NAME CHANGED -->
|
||||
<div v-else-if="event.getType() == 'm.room.name'" class="statusEvent">
|
||||
{{ stateEventDisplayName(event) }} changed room name to {{ event.getContent().name }}
|
||||
</div>
|
||||
|
||||
<!-- ROOM TOPIC CHANGED -->
|
||||
<div v-else-if="event.getType() == 'm.room.topic'" class="statusEvent">
|
||||
{{ stateEventDisplayName(event) }} changed topic to {{ event.getContent().topic }}
|
||||
</div>
|
||||
|
||||
<!-- ROOM AVATAR CHANGED -->
|
||||
<div v-else-if="event.getType() == 'm.room.avatar'" class="statusEvent">
|
||||
{{ stateEventDisplayName(event) }} changed the room avatar
|
||||
</div>
|
||||
|
||||
<div v-else class="statusEvent">Event: {{ event.getType() }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- CONTACT IS TYPING -->
|
||||
<div v-show="contactIsTyping" class="typing">Someone is typing...</div>
|
||||
|
|
@ -98,15 +117,59 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { TimelineWindow, EventTimeline } from "matrix-js-sdk";
|
||||
|
||||
// from https://kirbysayshi.com/2013/08/19/maintaining-scroll-position-knockoutjs-list.html
|
||||
function ScrollPosition(node) {
|
||||
this.node = node;
|
||||
this.previousScrollHeightMinusTop = 0;
|
||||
this.readyFor = "up";
|
||||
}
|
||||
|
||||
ScrollPosition.prototype.restore = function () {
|
||||
if (this.readyFor === "up") {
|
||||
this.node.scrollTop =
|
||||
this.node.scrollHeight - this.previousScrollHeightMinusTop;
|
||||
}
|
||||
|
||||
// 'down' doesn't need to be special cased unless the
|
||||
// content was flowing upwards, which would only happen
|
||||
// if the container is position: absolute, bottom: 0 for
|
||||
// a Facebook messages effect
|
||||
};
|
||||
|
||||
ScrollPosition.prototype.prepareFor = function (direction) {
|
||||
this.readyFor = direction || "up";
|
||||
this.previousScrollHeightMinusTop =
|
||||
this.node.scrollHeight - this.node.scrollTop;
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "Chat",
|
||||
|
||||
data: () => ({
|
||||
room: null,
|
||||
events: [],
|
||||
currentInput: "",
|
||||
contactIsTyping: false,
|
||||
timelineWindow: null,
|
||||
scrollPosition: null,
|
||||
}),
|
||||
|
||||
mounted() {
|
||||
const container = this.$refs.chatContainer;
|
||||
this.scrollPosition = new ScrollPosition(container);
|
||||
|
||||
this.$matrix.on("Room.timeline", this.onEvent);
|
||||
this.$matrix.on("RoomMember.typing", this.onUserTyping);
|
||||
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
this.$matrix.off("Room.timeline", this.onEvent);
|
||||
this.$matrix.off("RoomMember.typing", this.onUserTyping);
|
||||
},
|
||||
|
||||
computed: {
|
||||
myUserId() {
|
||||
return this.$store.state.auth.user.user_id;
|
||||
|
|
@ -125,95 +188,82 @@ export default {
|
|||
|
||||
// Clear old events
|
||||
this.events = [];
|
||||
|
||||
// Remove all old room listeners
|
||||
this.$matrix.off("Room.timeline", this.onEvent);
|
||||
this.$matrix.off("RoomMember.typing", this.onUserTyping);
|
||||
|
||||
this.timelineWindow = null;
|
||||
this.contactIsTyping = false;
|
||||
|
||||
if (!this.roomId) {
|
||||
return; // no room
|
||||
}
|
||||
const room = this.$matrix.getRoom(this.roomId);
|
||||
if (!room) {
|
||||
|
||||
this.room = this.$matrix.getRoom(this.roomId);
|
||||
if (!this.room) {
|
||||
return; // Not found
|
||||
}
|
||||
|
||||
room.timeline.forEach((event) => {
|
||||
this.handleMatrixEvent(event);
|
||||
this.timelineWindow = new TimelineWindow(
|
||||
this.$matrix.matrixClient,
|
||||
this.room.getUnfilteredTimelineSet(),
|
||||
{}
|
||||
);
|
||||
this.timelineWindow.load(null, 20).then(() => {
|
||||
this.events = this.timelineWindow.getEvents();
|
||||
this.paginateBackIfNeeded();
|
||||
});
|
||||
|
||||
// Add event listener for this room
|
||||
this.$matrix.on("Room.timeline", this.onEvent);
|
||||
this.$matrix.on("RoomMember.typing", this.onUserTyping);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
paginateBackIfNeeded() {
|
||||
this.$nextTick(() => {
|
||||
const container = this.$refs.chatContainer;
|
||||
if (container.scrollHeight <= container.clientHeight) {
|
||||
this.handleScrolledToTop();
|
||||
}
|
||||
})
|
||||
},
|
||||
onScroll(ignoredevent) {
|
||||
const container = this.$refs.chatContainer;
|
||||
if (container.scrollTop == 0) {
|
||||
// Scrolled to top
|
||||
this.handleScrolledToTop();
|
||||
} else if (
|
||||
container.scrollHeight - container.scrollTop ==
|
||||
container.clientHeight
|
||||
) {
|
||||
this.handleScrolledToBottom();
|
||||
}
|
||||
},
|
||||
onEvent(event) {
|
||||
if (event.getRoomId() !== this.roomId) {
|
||||
return; // Not for this room
|
||||
}
|
||||
if (this.handleMatrixEvent(event)) {
|
||||
this.$nextTick(function () {
|
||||
const container = this.$refs.chatContainer;
|
||||
if (container.children.length > 0) {
|
||||
const lastChild = container.children[container.children.length - 1];
|
||||
console.log("Scroll into view", lastChild);
|
||||
window.requestAnimationFrame(() => {
|
||||
lastChild.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "end",
|
||||
inline: "nearest",
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
this.paginateBackIfNeeded();
|
||||
},
|
||||
|
||||
onUserTyping(event) {
|
||||
//TODO
|
||||
if (event.getRoomId() !== this.roomId) {
|
||||
return; // Not for this room
|
||||
}
|
||||
console.log("Typing:", event);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle a matrix event. Add it to our array if valid type.
|
||||
* @returns True if the event was added, false otherwise.
|
||||
*/
|
||||
handleMatrixEvent(event) {
|
||||
console.log("Type is", event.getType());
|
||||
|
||||
if (event.getType() == "m.room.encryption") {
|
||||
this.$root.matrixClient.setRoomEncryption(
|
||||
event.event.room_id,
|
||||
event.getContent()
|
||||
);
|
||||
}
|
||||
|
||||
const allowedEvents = [
|
||||
"m.room.message",
|
||||
"m.room.member",
|
||||
"m.room.encrypted",
|
||||
];
|
||||
if (allowedEvents.includes(event.getType())) {
|
||||
console.log("Add event", event);
|
||||
this.events.push(event);
|
||||
return true;
|
||||
} else {
|
||||
console.log("Ignore event", event);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a display name given an event.
|
||||
*/
|
||||
stateEventDisplayName(event) {
|
||||
if (this.room) {
|
||||
const member = this.room.getMember(event.getSender());
|
||||
if (member) {
|
||||
return member.name;
|
||||
}
|
||||
}
|
||||
return event.getContent().displayname || event.event.state_key;
|
||||
},
|
||||
|
||||
messageEventDisplayName(event) {
|
||||
return this.stateEventDisplayName(event);
|
||||
},
|
||||
|
||||
sendMessage() {
|
||||
if (this.currentInput.length > 0) {
|
||||
this.sendMatrixMessage(this.currentInput);
|
||||
|
|
@ -222,17 +272,12 @@ export default {
|
|||
},
|
||||
|
||||
sendMatrixMessage(body) {
|
||||
// Send chat message sent event
|
||||
window.logtag("event", "chat_message_sent", {
|
||||
event_category: "chat",
|
||||
});
|
||||
|
||||
var content = {
|
||||
body: body,
|
||||
msgtype: "m.notice",
|
||||
msgtype: "m.text",
|
||||
};
|
||||
this.$root.matrixClient.sendEvent(
|
||||
this.currentRoomId,
|
||||
this.$matrix.matrixClient.sendEvent(
|
||||
this.roomId,
|
||||
"m.room.message",
|
||||
content,
|
||||
"",
|
||||
|
|
@ -257,6 +302,33 @@ export default {
|
|||
}
|
||||
return date.toLocaleString();
|
||||
},
|
||||
|
||||
handleScrolledToTop() {
|
||||
console.log("@top");
|
||||
// const room = this.$matrix.getRoom(this.roomId);
|
||||
if (
|
||||
this.timelineWindow &&
|
||||
this.timelineWindow.canPaginate(EventTimeline.BACKWARDS)
|
||||
) {
|
||||
this.timelineWindow
|
||||
.paginate(EventTimeline.BACKWARDS, 10, true)
|
||||
.then((success) => {
|
||||
if (success) {
|
||||
this.scrollPosition.prepareFor("up");
|
||||
this.events = this.timelineWindow.getEvents();
|
||||
this.$nextTick(() => {
|
||||
// restore scroll position!
|
||||
console.log("Restore scroll!");
|
||||
this.scrollPosition.restore();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
handleScrolledToBottom() {
|
||||
console.log("@bottom");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<v-list flat>
|
||||
<v-list dense>
|
||||
<v-subheader>ROOMS</v-subheader>
|
||||
<v-list-item-group v-model="currentRoomId" color="primary">
|
||||
<v-list-item v-for="room in $matrix.rooms" :key="room.roomId" :value="room.roomId">
|
||||
<v-list-item-icon>
|
||||
<v-icon v-text="room.icon"></v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-avatar>
|
||||
<v-img :src="room.avatar" />
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ room.summary.info.title }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ room.topic }}</v-list-item-subtitle>
|
||||
|
|
|
|||
|
|
@ -99,7 +99,8 @@ export default {
|
|||
store: matrixStore,
|
||||
sessionStore: webStorageSessionStore,
|
||||
deviceId: user.device_id,
|
||||
accessToken: user.access_token
|
||||
accessToken: user.access_token,
|
||||
timelineSupport: true
|
||||
}
|
||||
this.matrixClient = sdk.createClient(opts);
|
||||
return this.matrixClient
|
||||
|
|
@ -147,17 +148,30 @@ export default {
|
|||
},
|
||||
|
||||
onEvent(event) {
|
||||
if (event.getType() == "m.room.topic") {
|
||||
switch (event.getType()) {
|
||||
case "m.room.topic": {
|
||||
const room = this.matrixClient.getRoom(event.getRoomId());
|
||||
if (room) {
|
||||
Vue.set(room, "topic", event.getContent().topic);
|
||||
}
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
case "m.room.avatar": {
|
||||
const room = this.matrixClient.getRoom(event.getRoomId());
|
||||
if (room) {
|
||||
Vue.set(room, "avatar", room.getAvatarUrl(this.matrixClient.getHomeserverUrl(), 80, 80, "scale", true));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
reloadRooms() {
|
||||
this.rooms = this.matrixClient.getVisibleRooms();
|
||||
this.rooms.forEach(room => {
|
||||
Vue.set(room, "avatar", room.getAvatarUrl(this.matrixClient.getHomeserverUrl(), 80, 80, "scale", true));
|
||||
});
|
||||
},
|
||||
|
||||
setCurrentRoomId(roomId) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue