Timeline window and initial back paging

This commit is contained in:
N-Pex 2020-11-11 17:35:14 +01:00
parent 854a5ec770
commit 6c563b1e51
4 changed files with 178 additions and 83 deletions

View file

@ -128,7 +128,7 @@ $chat-text-size: 0.7pt;
} }
} }
.sender { .sender, .status {
font-family: 'Titillium Web', sans-serif; font-family: 'Titillium Web', sans-serif;
font-weight: 300; font-weight: 300;
font-size: 15 * $chat-text-size; font-size: 15 * $chat-text-size;
@ -151,3 +151,12 @@ $chat-text-size: 0.7pt;
text-align: center; text-align: center;
color: #1c242a; 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;
}

View file

@ -4,8 +4,9 @@
class="chat-content flex-grow-1 flex-shrink-1" class="chat-content flex-grow-1 flex-shrink-1"
ref="chatContainer" ref="chatContainer"
style="overflow-x: hidden; overflow-y: auto" 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 --> <!-- Contact joined the chat -->
<div <div
class="messageJoin" class="messageJoin"
@ -44,12 +45,11 @@
<div <div
v-else-if=" v-else-if="
event.getSender() != myUserId && event.getSender() != myUserId && event.getType() == 'm.room.message'
event.getType() == 'm.room.message'
" "
> >
<div class="messageIn"> <div class="messageIn">
<div class="sender">{{ event.getSender() }}</div> <div class="sender">{{ messageEventDisplayName(event) }}</div>
<div class="bubble"> <div class="bubble">
<div class="message">{{ event.getContent().body }}</div> <div class="message">{{ event.getContent().body }}</div>
</div> </div>
@ -64,11 +64,30 @@
<div class="bubble"> <div class="bubble">
<div class="message">{{ event.getContent().body }}</div> <div class="message">{{ event.getContent().body }}</div>
</div> </div>
<div class="status">{{ event.status }}</div>
</div> </div>
<div class="time"> <div class="time">
{{ formatTime(event.event.origin_server_ts) }} {{ formatTime(event.event.origin_server_ts) }}
</div> </div>
</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> </div>
<!-- CONTACT IS TYPING --> <!-- CONTACT IS TYPING -->
<div v-show="contactIsTyping" class="typing">Someone is typing...</div> <div v-show="contactIsTyping" class="typing">Someone is typing...</div>
@ -98,15 +117,59 @@
</template> </template>
<script> <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 { export default {
name: "Chat", name: "Chat",
data: () => ({ data: () => ({
room: null,
events: [], events: [],
currentInput: "", currentInput: "",
contactIsTyping: false, 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: { computed: {
myUserId() { myUserId() {
return this.$store.state.auth.user.user_id; return this.$store.state.auth.user.user_id;
@ -125,95 +188,82 @@ export default {
// Clear old events // Clear old events
this.events = []; this.events = [];
this.timelineWindow = null;
// Remove all old room listeners
this.$matrix.off("Room.timeline", this.onEvent);
this.$matrix.off("RoomMember.typing", this.onUserTyping);
this.contactIsTyping = false; this.contactIsTyping = false;
if (!this.roomId) { if (!this.roomId) {
return; // no room 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 return; // Not found
} }
room.timeline.forEach((event) => { this.timelineWindow = new TimelineWindow(
this.handleMatrixEvent(event); 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: { 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) { onEvent(event) {
if (event.getRoomId() !== this.roomId) { if (event.getRoomId() !== this.roomId) {
return; // Not for this room return; // Not for this room
} }
if (this.handleMatrixEvent(event)) { this.paginateBackIfNeeded();
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",
});
});
}
});
}
}, },
onUserTyping(event) { onUserTyping(event) {
//TODO if (event.getRoomId() !== this.roomId) {
return; // Not for this room
}
console.log("Typing:", event); 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. * Get a display name given an event.
*/ */
stateEventDisplayName(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; return event.getContent().displayname || event.event.state_key;
}, },
messageEventDisplayName(event) {
return this.stateEventDisplayName(event);
},
sendMessage() { sendMessage() {
if (this.currentInput.length > 0) { if (this.currentInput.length > 0) {
this.sendMatrixMessage(this.currentInput); this.sendMatrixMessage(this.currentInput);
@ -222,17 +272,12 @@ export default {
}, },
sendMatrixMessage(body) { sendMatrixMessage(body) {
// Send chat message sent event
window.logtag("event", "chat_message_sent", {
event_category: "chat",
});
var content = { var content = {
body: body, body: body,
msgtype: "m.notice", msgtype: "m.text",
}; };
this.$root.matrixClient.sendEvent( this.$matrix.matrixClient.sendEvent(
this.currentRoomId, this.roomId,
"m.room.message", "m.room.message",
content, content,
"", "",
@ -257,6 +302,33 @@ export default {
} }
return date.toLocaleString(); 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> </script>

View file

@ -1,11 +1,11 @@
<template> <template>
<v-list flat> <v-list dense>
<v-subheader>ROOMS</v-subheader> <v-subheader>ROOMS</v-subheader>
<v-list-item-group v-model="currentRoomId" color="primary"> <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 v-for="room in $matrix.rooms" :key="room.roomId" :value="room.roomId">
<v-list-item-icon> <v-list-item-avatar>
<v-icon v-text="room.icon"></v-icon> <v-img :src="room.avatar" />
</v-list-item-icon> </v-list-item-avatar>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>{{ room.summary.info.title }}</v-list-item-title> <v-list-item-title>{{ room.summary.info.title }}</v-list-item-title>
<v-list-item-subtitle>{{ room.topic }}</v-list-item-subtitle> <v-list-item-subtitle>{{ room.topic }}</v-list-item-subtitle>

View file

@ -99,7 +99,8 @@ export default {
store: matrixStore, store: matrixStore,
sessionStore: webStorageSessionStore, sessionStore: webStorageSessionStore,
deviceId: user.device_id, deviceId: user.device_id,
accessToken: user.access_token accessToken: user.access_token,
timelineSupport: true
} }
this.matrixClient = sdk.createClient(opts); this.matrixClient = sdk.createClient(opts);
return this.matrixClient return this.matrixClient
@ -147,17 +148,30 @@ export default {
}, },
onEvent(event) { onEvent(event) {
if (event.getType() == "m.room.topic") { switch (event.getType()) {
const room = this.matrixClient.getRoom(event.getRoomId()); case "m.room.topic": {
if (room) { const room = this.matrixClient.getRoom(event.getRoomId());
Vue.set(room, "topic", event.getContent().topic); 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() { reloadRooms() {
this.rooms = this.matrixClient.getVisibleRooms(); this.rooms = this.matrixClient.getVisibleRooms();
this.rooms.forEach(room => {
Vue.set(room, "avatar", room.getAvatarUrl(this.matrixClient.getHomeserverUrl(), 80, 80, "scale", true));
});
}, },
setCurrentRoomId(roomId) { setCurrentRoomId(roomId) {