Merge remote-tracking branch 'origin' into joriks/eslint-config

This commit is contained in:
Jorik Schellekens 2020-06-29 16:49:33 +01:00
commit b110639c76
103 changed files with 3118 additions and 1035 deletions

View file

@ -1,135 +0,0 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Room } from "matrix-js-sdk/src/models/room";
import { ActionPayload } from "../dispatcher/payloads";
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
import defaultDispatcher from "../dispatcher/dispatcher";
import { RoomListStoreTempProxy } from "./room-list/RoomListStoreTempProxy";
import { textForEvent } from "../TextForEvent";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { _t } from "../languageHandler";
const PREVIEWABLE_EVENTS = [
// This is the same list from RiotX
{type: "m.room.message", isState: false},
{type: "m.room.name", isState: true},
{type: "m.room.topic", isState: true},
{type: "m.room.member", isState: true},
{type: "m.room.history_visibility", isState: true},
{type: "m.call.invite", isState: false},
{type: "m.call.hangup", isState: false},
{type: "m.call.answer", isState: false},
{type: "m.room.encrypted", isState: false},
{type: "m.room.encryption", isState: true},
{type: "m.room.third_party_invite", isState: true},
{type: "m.sticker", isState: false},
{type: "m.room.create", isState: true},
];
// The maximum number of events we're willing to look back on to get a preview.
const MAX_EVENTS_BACKWARDS = 50;
interface IState {
[roomId: string]: string | null; // null indicates the preview is empty
}
export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
private static internalInstance = new MessagePreviewStore();
private constructor() {
super(defaultDispatcher, {});
}
public static get instance(): MessagePreviewStore {
return MessagePreviewStore.internalInstance;
}
/**
* Gets the pre-translated preview for a given room
* @param room The room to get the preview for.
* @returns {string} The preview, or null if none present.
*/
public getPreviewForRoom(room: Room): string {
if (!room) return null; // invalid room, just return nothing
// It's faster to do a lookup this way than it is to use Object.keys().includes()
// We only want to generate a preview if there's one actually missing and not explicitly
// set as 'none'.
const val = this.state[room.roomId];
if (val !== null && typeof(val) !== "string") {
this.generatePreview(room);
}
return this.state[room.roomId];
}
private generatePreview(room: Room) {
const timeline = room.getLiveTimeline();
if (!timeline) return; // usually only happens in tests
const events = timeline.getEvents();
for (let i = events.length - 1; i >= 0; i--) {
if (i === events.length - MAX_EVENTS_BACKWARDS) return; // limit reached
const event = events[i];
const preview = this.generatePreviewForEvent(event);
if (preview.isPreviewable) {
// noinspection JSIgnoredPromiseFromCall - the AsyncStore handles concurrent calls
this.updateState({[room.roomId]: preview.preview});
return; // break - we found some text
}
}
// if we didn't find anything, subscribe ourselves to an update
// noinspection JSIgnoredPromiseFromCall - the AsyncStore handles concurrent calls
this.updateState({[room.roomId]: null});
}
protected async onAction(payload: ActionPayload) {
if (!this.matrixClient) return;
// TODO: Remove when new room list is made the default
if (!RoomListStoreTempProxy.isUsingNewStore()) return;
if (payload.action === 'MatrixActions.Room.timeline' || payload.action === 'MatrixActions.Event.decrypted') {
const event = payload.event; // TODO: Type out the dispatcher
if (!Object.keys(this.state).includes(event.getRoomId())) return; // not important
const preview = this.generatePreviewForEvent(event);
if (preview.isPreviewable) {
await this.updateState({[event.getRoomId()]: preview.preview});
return; // break - we found some text
}
}
}
private generatePreviewForEvent(event: MatrixEvent): { isPreviewable: boolean, preview: string } {
if (PREVIEWABLE_EVENTS.some(p => p.type === event.getType() && p.isState === event.isState())) {
const isSelf = event.getSender() === this.matrixClient.getUserId();
let text = textForEvent(event, /*skipUserPrefix=*/isSelf);
if (!text || text.trim().length === 0) text = null; // force null if useless to us
if (text && isSelf) {
// XXX: i18n doesn't really work here if the language doesn't support prefixing.
// We'd ideally somehow route the `You:` bit to the textForEvent call, however
// threading that through is non-trivial.
text = _t("You: %(message)s", {message: text});
}
return {isPreviewable: true, preview: text};
}
return {isPreviewable: false, preview: null};
}
}

View file

@ -0,0 +1,122 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { ActionPayload } from "../dispatcher/payloads";
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
import defaultDispatcher from "../dispatcher/dispatcher";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { User } from "matrix-js-sdk/src/models/user";
import { throttle } from "lodash";
import { MatrixClientPeg } from "../MatrixClientPeg";
import { _t } from "../languageHandler";
interface IState {
displayName?: string;
avatarUrl?: string;
}
export class OwnProfileStore extends AsyncStoreWithClient<IState> {
private static internalInstance = new OwnProfileStore();
private monitoredUser: User;
private constructor() {
super(defaultDispatcher, {});
}
public static get instance(): OwnProfileStore {
return OwnProfileStore.internalInstance;
}
/**
* Gets the display name for the user, or null if not present.
*/
public get displayName(): string {
if (!this.matrixClient) return this.state.displayName || null;
if (this.matrixClient.isGuest()) {
return _t("Guest");
} else if (this.state.displayName) {
return this.state.displayName;
} else {
return this.matrixClient.getUserId();
}
}
/**
* Gets the MXC URI of the user's avatar, or null if not present.
*/
public get avatarMxc(): string {
return this.state.avatarUrl || null;
}
/**
* Gets the user's avatar as an HTTP URL of the given size. If the user's
* avatar is not present, this returns null.
* @param size The size of the avatar
* @returns The HTTP URL of the user's avatar
*/
public getHttpAvatarUrl(size: number): string {
if (!this.avatarMxc) return null;
return this.matrixClient.mxcUrlToHttp(this.avatarMxc, size, size);
}
protected async onNotReady() {
if (this.monitoredUser) {
this.monitoredUser.removeListener("User.displayName", this.onProfileUpdate);
this.monitoredUser.removeListener("User.avatarUrl", this.onProfileUpdate);
}
if (this.matrixClient) {
this.matrixClient.removeListener("RoomState.events", this.onStateEvents);
}
await this.reset({});
}
protected async onReady() {
const myUserId = this.matrixClient.getUserId();
this.monitoredUser = this.matrixClient.getUser(myUserId);
if (this.monitoredUser) {
this.monitoredUser.on("User.displayName", this.onProfileUpdate);
this.monitoredUser.on("User.avatarUrl", this.onProfileUpdate);
}
// We also have to listen for membership events for ourselves as the above User events
// are fired only with presence, which matrix.org (and many others) has disabled.
this.matrixClient.on("RoomState.events", this.onStateEvents);
await this.onProfileUpdate(); // trigger an initial update
}
protected async onAction(payload: ActionPayload) {
// we don't actually do anything here
}
private onProfileUpdate = async () => {
// We specifically do not use the User object we stored for profile info as it
// could easily be wrong (such as per-room instead of global profile).
const profileInfo = await this.matrixClient.getProfileInfo(this.matrixClient.getUserId());
await this.updateState({displayName: profileInfo.displayname, avatarUrl: profileInfo.avatar_url});
};
// TSLint wants this to be a member, but we don't want that.
// tslint:disable-next-line
private onStateEvents = throttle(async (ev: MatrixEvent) => {
const myUserId = MatrixClientPeg.get().getUserId();
if (ev.getType() === 'm.room.member' && ev.getSender() === myUserId && ev.getStateKey() === myUserId) {
await this.onProfileUpdate();
}
}, 200, {trailing: true, leading: true});
}

View file

@ -18,6 +18,10 @@ import { TagID } from "./models";
const TILE_HEIGHT_PX = 44;
// the .65 comes from the CSS where the show more button is
// mathematically 65% of a tile when floating.
const RESIZER_BOX_FACTOR = 0.65;
interface ISerializedListLayout {
numTiles: number;
showPreviews: boolean;
@ -67,6 +71,7 @@ export class ListLayout {
}
public get visibleTiles(): number {
if (this._n === 0) return this.defaultVisibleTiles;
return Math.max(this._n, this.minVisibleTiles);
}
@ -76,9 +81,13 @@ export class ListLayout {
}
public get minVisibleTiles(): number {
// the .65 comes from the CSS where the show more button is
// mathematically 65% of a tile when floating.
return 4.65;
return 1 + RESIZER_BOX_FACTOR;
}
public get defaultVisibleTiles(): number {
// TODO: Remove dogfood flag
const val = Number(localStorage.getItem("mx_dogfood_rl_defTiles") || 4);
return val + RESIZER_BOX_FACTOR;
}
public calculateTilesToPixelsMin(maxTiles: number, n: number, possiblePadding: number): number {
@ -92,6 +101,10 @@ export class ListLayout {
return this.tilesToPixels(Math.min(maxTiles, n)) + padding;
}
public tilesWithResizerBoxFactor(n: number): number {
return n + RESIZER_BOX_FACTOR;
}
public tilesWithPadding(n: number, paddingPx: number): number {
return this.pixelsToTiles(this.tilesToPixelsWithPadding(n, paddingPx));
}

View file

@ -0,0 +1,204 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Room } from "matrix-js-sdk/src/models/room";
import { ActionPayload } from "../../dispatcher/payloads";
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
import defaultDispatcher from "../../dispatcher/dispatcher";
import { RoomListStoreTempProxy } from "./RoomListStoreTempProxy";
import { MessageEventPreview } from "./previews/MessageEventPreview";
import { NameEventPreview } from "./previews/NameEventPreview";
import { TagID } from "./models";
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
import { TopicEventPreview } from "./previews/TopicEventPreview";
import { MembershipEventPreview } from "./previews/MembershipEventPreview";
import { HistoryVisibilityEventPreview } from "./previews/HistoryVisibilityEventPreview";
import { CallInviteEventPreview } from "./previews/CallInviteEventPreview";
import { CallAnswerEventPreview } from "./previews/CallAnswerEventPreview";
import { CallHangupEvent } from "./previews/CallHangupEvent";
import { EncryptionEventPreview } from "./previews/EncryptionEventPreview";
import { ThirdPartyInviteEventPreview } from "./previews/ThirdPartyInviteEventPreview";
import { StickerEventPreview } from "./previews/StickerEventPreview";
import { ReactionEventPreview } from "./previews/ReactionEventPreview";
import { CreationEventPreview } from "./previews/CreationEventPreview";
const PREVIEWS = {
'm.room.message': {
isState: false,
previewer: new MessageEventPreview(),
},
'm.room.name': {
isState: true,
previewer: new NameEventPreview(),
},
'm.room.topic': {
isState: true,
previewer: new TopicEventPreview(),
},
'm.room.member': {
isState: true,
previewer: new MembershipEventPreview(),
},
'm.room.history_visibility': {
isState: true,
previewer: new HistoryVisibilityEventPreview(),
},
'm.call.invite': {
isState: false,
previewer: new CallInviteEventPreview(),
},
'm.call.answer': {
isState: false,
previewer: new CallAnswerEventPreview(),
},
'm.call.hangup': {
isState: false,
previewer: new CallHangupEvent(),
},
'm.room.encryption': {
isState: true,
previewer: new EncryptionEventPreview(),
},
'm.room.third_party_invite': {
isState: true,
previewer: new ThirdPartyInviteEventPreview(),
},
'm.sticker': {
isState: false,
previewer: new StickerEventPreview(),
},
'm.reaction': {
isState: false,
previewer: new ReactionEventPreview(),
},
'm.room.create': {
isState: true,
previewer: new CreationEventPreview(),
},
};
// The maximum number of events we're willing to look back on to get a preview.
const MAX_EVENTS_BACKWARDS = 50;
// type merging ftw
type TAG_ANY = "im.vector.any";
const TAG_ANY: TAG_ANY = "im.vector.any";
interface IState {
[roomId: string]: Map<TagID | TAG_ANY, string | null>; // null indicates the preview is empty / irrelevant
}
export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
private static internalInstance = new MessagePreviewStore();
private constructor() {
super(defaultDispatcher, {});
}
public static get instance(): MessagePreviewStore {
return MessagePreviewStore.internalInstance;
}
/**
* Gets the pre-translated preview for a given room
* @param room The room to get the preview for.
* @param inTagId The tag ID in which the room resides
* @returns The preview, or null if none present.
*/
public getPreviewForRoom(room: Room, inTagId: TagID): string {
if (!room) return null; // invalid room, just return nothing
const val = this.state[room.roomId];
if (!val) this.generatePreview(room, inTagId);
const previews = this.state[room.roomId];
if (!previews) return null;
if (!previews.has(inTagId)) {
return previews.get(TAG_ANY);
}
return previews.get(inTagId);
}
private generatePreview(room: Room, tagId?: TagID) {
const events = room.timeline;
if (!events) return; // should only happen in tests
let map = this.state[room.roomId];
if (!map) {
map = new Map<TagID | TAG_ANY, string | null>();
// We set the state later with the map, so no need to send an update now
}
// Set the tags so we know what to generate
if (!map.has(TAG_ANY)) map.set(TAG_ANY, null);
if (tagId && !map.has(tagId)) map.set(tagId, null);
let changed = false;
for (let i = events.length - 1; i >= 0; i--) {
if (i === events.length - MAX_EVENTS_BACKWARDS) return; // limit reached
const event = events[i];
const previewDef = PREVIEWS[event.getType()];
if (!previewDef) continue;
if (previewDef.isState && isNullOrUndefined(event.getStateKey())) continue;
const anyPreview = previewDef.previewer.getTextFor(event, null);
if (!anyPreview) continue; // not previewable for some reason
changed = changed || anyPreview !== map.get(TAG_ANY);
map.set(TAG_ANY, anyPreview);
const tagsToGenerate = Array.from(map.keys()).filter(t => t !== TAG_ANY); // we did the any tag above
for (const genTagId of tagsToGenerate) {
const realTagId: TagID = genTagId === TAG_ANY ? null : genTagId;
const preview = previewDef.previewer.getTextFor(event, realTagId);
if (preview === anyPreview) {
changed = changed || anyPreview !== map.get(genTagId);
map.delete(genTagId);
} else {
changed = changed || preview !== map.get(genTagId);
map.set(genTagId, preview);
}
}
if (changed) {
// Update state for good measure - causes emit for update
// noinspection JSIgnoredPromiseFromCall - the AsyncStore handles concurrent calls
this.updateState({[room.roomId]: map});
}
return; // we're done
}
// At this point, we didn't generate a preview so clear it
// noinspection JSIgnoredPromiseFromCall - the AsyncStore handles concurrent calls
this.updateState({[room.roomId]: null});
}
protected async onAction(payload: ActionPayload) {
if (!this.matrixClient) return;
// TODO: Remove when new room list is made the default
if (!RoomListStoreTempProxy.isUsingNewStore()) return;
if (payload.action === 'MatrixActions.Room.timeline' || payload.action === 'MatrixActions.Event.decrypted') {
const event = payload.event; // TODO: Type out the dispatcher
if (!Object.keys(this.state).includes(event.getRoomId())) return; // not important
this.generatePreview(this.matrixClient.getRoom(event.getRoomId()), TAG_ANY);
}
}
}

View file

@ -158,12 +158,12 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
// First see if the receipt event is for our own user. If it was, trigger
// a room update (we probably read the room on a different device).
if (readReceiptChangeIsFor(payload.event, this.matrixClient)) {
console.log(`[RoomListDebug] Got own read receipt in ${payload.event.roomId}`);
const room = this.matrixClient.getRoom(payload.event.roomId);
const room = payload.room;
if (!room) {
console.warn(`Own read receipt was in unknown room ${payload.event.roomId}`);
console.warn(`Own read receipt was in unknown room ${room.roomId}`);
return;
}
console.log(`[RoomListDebug] Got own read receipt in ${room.roomId}`);
await this.handleRoomUpdate(room, RoomUpdateCause.ReadReceipt);
return;
}

View file

@ -170,12 +170,16 @@ export class Algorithm extends EventEmitter {
// When we do have a room though, we expect to be able to find it
const tag = this.roomIdsToTags[val.roomId][0];
if (!tag) throw new Error(`${val.roomId} does not belong to a tag and cannot be sticky`);
let position = this.cachedRooms[tag].indexOf(val);
// We specifically do NOT use the ordered rooms set as it contains the sticky room, which
// means we'll be off by 1 when the user is switching rooms. This leads to visual jumping
// when the user is moving south in the list (not north, because of math).
let position = this.getOrderedRoomsWithoutSticky()[tag].indexOf(val);
if (position < 0) throw new Error(`${val.roomId} does not appear to be known and cannot be sticky`);
// 🐉 Here be dragons.
// Before we can go through with lying to the underlying algorithm about a room
// we need to ensure that when we do we're ready for the innevitable sticky room
// we need to ensure that when we do we're ready for the inevitable sticky room
// update we'll receive. To prepare for that, we first remove the sticky room and
// recalculate the state ourselves so that when the underlying algorithm calls for
// the same thing it no-ops. After we're done calling the algorithm, we'll issue
@ -208,6 +212,12 @@ export class Algorithm extends EventEmitter {
position: position,
tag: tag,
};
// We update the filtered rooms just in case, as otherwise users will end up visiting
// a room while filtering and it'll disappear. We don't update the filter earlier in
// this function simply because we don't have to.
this.recalculateFilteredRoomsForTag(tag);
if (lastStickyRoom && lastStickyRoom.tag !== tag) this.recalculateFilteredRoomsForTag(lastStickyRoom.tag);
this.recalculateStickyRoom();
// Finally, trigger an update
@ -231,9 +241,7 @@ export class Algorithm extends EventEmitter {
// We optimize our lookups by trying to reduce sample size as much as possible
// to the rooms we know will be deduped by the Set.
const rooms = this.cachedRooms[tagId].map(r => r); // cheap clone
if (this._stickyRoom && this._stickyRoom.tag === tagId && this._stickyRoom.room) {
rooms.push(this._stickyRoom.room);
}
this.tryInsertStickyRoomToFilterSet(rooms, tagId);
let remainingRooms = rooms.map(r => r);
let allowedRoomsInThisTag = [];
let lastFilterPriority = orderedFilters[0].relativePriority;
@ -263,6 +271,7 @@ export class Algorithm extends EventEmitter {
this.emit(LIST_UPDATED_EVENT);
}
// TODO: Remove or use.
protected addPossiblyFilteredRoomsToTag(tagId: TagID, added: Room[]): void {
const filters = this.allowedByFilter.keys();
for (const room of added) {
@ -281,7 +290,8 @@ export class Algorithm extends EventEmitter {
protected recalculateFilteredRoomsForTag(tagId: TagID): void {
console.log(`Recalculating filtered rooms for ${tagId}`);
delete this.filteredRooms[tagId];
const rooms = this.cachedRooms[tagId];
const rooms = this.cachedRooms[tagId].map(r => r); // cheap clone
this.tryInsertStickyRoomToFilterSet(rooms, tagId);
const filteredRooms = rooms.filter(r => this.allowedRoomsByFilters.has(r));
if (filteredRooms.length > 0) {
this.filteredRooms[tagId] = filteredRooms;
@ -289,6 +299,17 @@ export class Algorithm extends EventEmitter {
console.log(`[DEBUG] ${filteredRooms.length}/${rooms.length} rooms filtered into ${tagId}`);
}
protected tryInsertStickyRoomToFilterSet(rooms: Room[], tagId: TagID) {
if (!this._stickyRoom || !this._stickyRoom.room || this._stickyRoom.tag !== tagId) return;
const position = this._stickyRoom.position;
if (position >= rooms.length) {
rooms.push(this._stickyRoom.room);
} else {
rooms.splice(position, 0, this._stickyRoom.room);
}
}
/**
* Recalculate the sticky room position. If this is being called in relation to
* a specific tag being updated, it should be given to this function to optimize
@ -377,6 +398,20 @@ export class Algorithm extends EventEmitter {
return this.filteredRooms;
}
/**
* This returns the same as getOrderedRooms(), but without the sticky room
* map as it causes issues for sticky room handling (see sticky room handling
* for more information).
* @returns {ITagMap} The cached list of rooms, ordered,
* for each tag. May be empty, but never null/undefined.
*/
private getOrderedRoomsWithoutSticky(): ITagMap {
if (!this.hasFilters) {
return this.cachedRooms;
}
return this.filteredRooms;
}
/**
* Seeds the Algorithm with a set of rooms. The algorithm will discard all
* previously known information and instead use these rooms instead.

View file

@ -0,0 +1,35 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { IPreview } from "./IPreview";
import { TagID } from "../models";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
import { _t } from "../../../languageHandler";
export class CallAnswerEventPreview implements IPreview {
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
if (shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
if (isSelf(event)) {
return _t("You joined the call");
} else {
return _t("%(senderName)s joined the call", {senderName: getSenderName(event)});
}
} else {
return _t("Call in progress");
}
}
}

View file

@ -0,0 +1,35 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { IPreview } from "./IPreview";
import { TagID } from "../models";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
import { _t } from "../../../languageHandler";
export class CallHangupEvent implements IPreview {
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
if (shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
if (isSelf(event)) {
return _t("You left the call");
} else {
return _t("%(senderName)s left the call", {senderName: getSenderName(event)});
}
} else {
return _t("Call ended");
}
}
}

View file

@ -0,0 +1,39 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { IPreview } from "./IPreview";
import { TagID } from "../models";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
import { _t } from "../../../languageHandler";
export class CallInviteEventPreview implements IPreview {
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
if (shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
if (isSelf(event)) {
return _t("You started a call");
} else {
return _t("%(senderName)s started a call", {senderName: getSenderName(event)});
}
} else {
if (isSelf(event)) {
return _t("Waiting for answer");
} else {
return _t("%(senderName)s is calling", {senderName: getSenderName(event)});
}
}
}
}

View file

@ -0,0 +1,31 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { IPreview } from "./IPreview";
import { TagID } from "../models";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { getSenderName, isSelf } from "./utils";
import { _t } from "../../../languageHandler";
export class CreationEventPreview implements IPreview {
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
if (isSelf(event)) {
return _t("You created the room");
} else {
return _t("%(senderName)s created the room", {senderName: getSenderName(event)});
}
}
}

View file

@ -0,0 +1,31 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { IPreview } from "./IPreview";
import { TagID } from "../models";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { getSenderName, isSelf } from "./utils";
import { _t } from "../../../languageHandler";
export class EncryptionEventPreview implements IPreview {
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
if (isSelf(event)) {
return _t("You made the chat encrypted");
} else {
return _t("%(senderName)s made the chat encrypted", {senderName: getSenderName(event)});
}
}
}

View file

@ -0,0 +1,42 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { IPreview } from "./IPreview";
import { TagID } from "../models";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { getSenderName, isSelf } from "./utils";
import { _t } from "../../../languageHandler";
export class HistoryVisibilityEventPreview implements IPreview {
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
const visibility = event.getContent()['history_visibility'];
const isUs = isSelf(event);
if (visibility === 'invited' || visibility === 'joined') {
return isUs
? _t("You made history visible to new members")
: _t("%(senderName)s made history visible to new members", {senderName: getSenderName(event)});
} else if (visibility === 'world_readable') {
return isUs
? _t("You made history visible to anyone")
: _t("%(senderName)s made history visible to anyone", {senderName: getSenderName(event)});
} else { // shared, default
return isUs
? _t("You made history visible to future members")
: _t("%(senderName)s made history visible to future members", {senderName: getSenderName(event)});
}
}
}

View file

@ -0,0 +1,31 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { TagID } from "../models";
/**
* Represents an event preview.
*/
export interface IPreview {
/**
* Gets the text which represents the event as a preview.
* @param event The event to preview.
* @param tagId Optional. The tag where the room the event was sent in resides.
* @returns The preview.
*/
getTextFor(event: MatrixEvent, tagId?: TagID): string;
}

View file

@ -0,0 +1,90 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { IPreview } from "./IPreview";
import { TagID } from "../models";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { getTargetName, isSelfTarget } from "./utils";
import { _t } from "../../../languageHandler";
export class MembershipEventPreview implements IPreview {
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
const newMembership = event.getContent()['membership'];
const oldMembership = event.getPrevContent()['membership'];
const reason = event.getContent()['reason'];
const isUs = isSelfTarget(event);
if (newMembership === 'invite') {
return isUs
? _t("You were invited")
: _t("%(targetName)s was invited", {targetName: getTargetName(event)});
} else if (newMembership === 'leave' && oldMembership !== 'invite') {
if (event.getSender() === event.getStateKey()) {
return isUs
? _t("You left")
: _t("%(targetName)s left", {targetName: getTargetName(event)});
} else {
if (reason) {
return isUs
? _t("You were kicked (%(reason)s)", {reason})
: _t("%(targetName)s was kicked (%(reason)s)", {targetName: getTargetName(event), reason});
} else {
return isUs
? _t("You were kicked")
: _t("%(targetName)s was kicked", {targetName: getTargetName(event)});
}
}
} else if (newMembership === 'leave' && oldMembership === 'invite') {
if (event.getSender() === event.getStateKey()) {
return isUs
? _t("You rejected the invite")
: _t("%(targetName)s rejected the invite", {targetName: getTargetName(event)});
} else {
return isUs
? _t("You were uninvited")
: _t("%(targetName)s was uninvited", {targetName: getTargetName(event)});
}
} else if (newMembership === 'ban') {
if (reason) {
return isUs
? _t("You were banned (%(reason)s)", {reason})
: _t("%(targetName)s was banned (%(reason)s)", {targetName: getTargetName(event), reason});
} else {
return isUs
? _t("You were banned")
: _t("%(targetName)s was banned", {targetName: getTargetName(event)});
}
} else if (newMembership === 'join' && oldMembership !== 'join') {
return isUs
? _t("You joined")
: _t("%(targetName)s joined", {targetName: getTargetName(event)});
} else {
const isDisplayNameChange = event.getContent()['displayname'] !== event.getPrevContent()['displayname'];
const isAvatarChange = event.getContent()['avatar_url'] !== event.getPrevContent()['avatar_url'];
if (isDisplayNameChange) {
return isUs
? _t("You changed your name")
: _t("%(targetName)s changed their name", {targetName: getTargetName(event)});
} else if (isAvatarChange) {
return isUs
? _t("You changed your avatar")
: _t("%(targetName)s changed their avatar", {targetName: getTargetName(event)});
} else {
return null; // no change
}
}
}
}

View file

@ -0,0 +1,55 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { IPreview } from "./IPreview";
import { TagID } from "../models";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { _t } from "../../../languageHandler";
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
import ReplyThread from "../../../components/views/elements/ReplyThread";
export class MessageEventPreview implements IPreview {
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
let eventContent = event.getContent();
if (event.isRelation("m.replace")) {
// It's an edit, generate the preview on the new text
eventContent = event.getContent()['m.new_content'];
}
let body = (eventContent['body'] || '').trim();
const msgtype = eventContent['msgtype'];
if (!body || !msgtype) return null; // invalid event, no preview
// XXX: Newer relations have a getRelation() function which is not compatible with replies.
const mRelatesTo = event.getWireContent()['m.relates_to'];
if (mRelatesTo && mRelatesTo['m.in_reply_to']) {
// If this is a reply, get the real reply and use that
body = (ReplyThread.stripPlainReply(body) || '').trim();
if (!body) return null; // invalid event, no preview
}
if (msgtype === 'm.emote') {
return _t("%(senderName)s %(emote)s", {senderName: getSenderName(event), emote: body});
}
if (isSelf(event) || !shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
return body;
} else {
return _t("%(senderName)s: %(message)s", {senderName: getSenderName(event), message: body});
}
}
}

View file

@ -0,0 +1,31 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { IPreview } from "./IPreview";
import { TagID } from "../models";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { getSenderName, isSelf } from "./utils";
import { _t } from "../../../languageHandler";
export class NameEventPreview implements IPreview {
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
if (isSelf(event)) {
return _t("You changed the room name");
} else {
return _t("%(senderName)s changed the room name", {senderName: getSenderName(event)});
}
}
}

View file

@ -0,0 +1,34 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { IPreview } from "./IPreview";
import { TagID } from "../models";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
import { _t } from "../../../languageHandler";
export class ReactionEventPreview implements IPreview {
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
const reaction = event.getRelation().key;
if (!reaction) return;
if (isSelf(event) || !shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
return reaction;
} else {
return _t("%(senderName)s: %(reaction)s", {senderName: getSenderName(event), reaction});
}
}
}

View file

@ -0,0 +1,34 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { IPreview } from "./IPreview";
import { TagID } from "../models";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
import { _t } from "../../../languageHandler";
export class StickerEventPreview implements IPreview {
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
const stickerName = event.getContent()['body'];
if (!stickerName) return null;
if (isSelf(event) || !shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
return stickerName;
} else {
return _t("%(senderName)s: %(stickerName)s", {senderName: getSenderName(event), stickerName});
}
}
}

View file

@ -0,0 +1,42 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { IPreview } from "./IPreview";
import { TagID } from "../models";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { getSenderName, isSelf } from "./utils";
import { _t } from "../../../languageHandler";
import { isValid3pidInvite } from "../../../RoomInvite";
export class ThirdPartyInviteEventPreview implements IPreview {
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
if (!isValid3pidInvite(event)) {
const targetName = event.getPrevContent().display_name || _t("Someone");
if (isSelf(event)) {
return _t("You uninvited %(targetName)s", {targetName});
} else {
return _t("%(senderName)s uninvited %(targetName)s", {senderName: getSenderName(event), targetName});
}
} else {
const targetName = event.getContent().display_name;
if (isSelf(event)) {
return _t("You invited %(targetName)s", {targetName});
} else {
return _t("%(senderName)s invited %(targetName)s", {senderName: getSenderName(event), targetName});
}
}
}
}

View file

@ -0,0 +1,31 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { IPreview } from "./IPreview";
import { TagID } from "../models";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { getSenderName, isSelf } from "./utils";
import { _t } from "../../../languageHandler";
export class TopicEventPreview implements IPreview {
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
if (isSelf(event)) {
return _t("You changed the room topic");
} else {
return _t("%(senderName)s changed the room topic", {senderName: getSenderName(event)});
}
}
}

View file

@ -0,0 +1,49 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { DefaultTagID, TagID } from "../models";
export function isSelf(event: MatrixEvent): boolean {
const selfUserId = MatrixClientPeg.get().getUserId();
if (event.getType() === 'm.room.member') {
return event.getStateKey() === selfUserId;
}
return event.getSender() === selfUserId;
}
export function isSelfTarget(event: MatrixEvent): boolean {
const selfUserId = MatrixClientPeg.get().getUserId();
return event.getStateKey() === selfUserId;
}
export function shouldPrefixMessagesIn(roomId: string, tagId: TagID): boolean {
if (tagId !== DefaultTagID.DM) return true;
// We don't prefix anything in 1:1s
const room = MatrixClientPeg.get().getRoom(roomId);
if (!room) return true;
return room.currentState.getJoinedMemberCount() !== 2;
}
export function getSenderName(event: MatrixEvent): string {
return event.sender ? event.sender.name : event.getSender();
}
export function getTargetName(event: MatrixEvent): string {
return event.target ? event.target.name : event.getStateKey();
}