mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2025-01-11 04:27:40 +01:00
Merge pull request #346 from vector-im/bwindels/leave-room
Leave and forget room
This commit is contained in:
commit
2ccd0c8def
@ -132,6 +132,22 @@ export class RoomViewModel extends ViewModel {
|
|||||||
return this.name;
|
return this.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get canLeave() {
|
||||||
|
return this._room.isJoined;
|
||||||
|
}
|
||||||
|
|
||||||
|
leaveRoom() {
|
||||||
|
this._room.leave();
|
||||||
|
}
|
||||||
|
|
||||||
|
get canForget() {
|
||||||
|
return this._room.isArchived;
|
||||||
|
}
|
||||||
|
|
||||||
|
forgetRoom() {
|
||||||
|
this._room.forget();
|
||||||
|
}
|
||||||
|
|
||||||
async _sendMessage(message) {
|
async _sendMessage(message) {
|
||||||
if (!this._room.isArchived && message) {
|
if (!this._room.isArchived && message) {
|
||||||
try {
|
try {
|
||||||
@ -262,7 +278,6 @@ export class RoomViewModel extends ViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
get composerViewModel() {
|
get composerViewModel() {
|
||||||
return this._composerVM;
|
return this._composerVM;
|
||||||
}
|
}
|
||||||
|
@ -85,6 +85,7 @@ export class Session {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
this._createRoomEncryption = this._createRoomEncryption.bind(this);
|
this._createRoomEncryption = this._createRoomEncryption.bind(this);
|
||||||
|
this._forgetArchivedRoom = this._forgetArchivedRoom.bind(this);
|
||||||
this.needsSessionBackup = new ObservableValue(false);
|
this.needsSessionBackup = new ObservableValue(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -407,6 +408,7 @@ export class Session {
|
|||||||
storage: this._storage,
|
storage: this._storage,
|
||||||
emitCollectionChange: () => {},
|
emitCollectionChange: () => {},
|
||||||
releaseCallback: () => this._activeArchivedRooms.delete(roomId),
|
releaseCallback: () => this._activeArchivedRooms.delete(roomId),
|
||||||
|
forgetCallback: this._forgetArchivedRoom,
|
||||||
hsApi: this._hsApi,
|
hsApi: this._hsApi,
|
||||||
mediaRepository: this._mediaRepository,
|
mediaRepository: this._mediaRepository,
|
||||||
user: this._user,
|
user: this._user,
|
||||||
@ -555,6 +557,13 @@ export class Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_forgetArchivedRoom(roomId) {
|
||||||
|
const statusObservable = this._observedRoomStatus.get(roomId);
|
||||||
|
if (statusObservable) {
|
||||||
|
statusObservable.set(statusObservable.get().withoutArchived());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
get syncToken() {
|
get syncToken() {
|
||||||
return this._syncInfo?.token;
|
return this._syncInfo?.token;
|
||||||
|
@ -193,6 +193,10 @@ export class HomeServerApi {
|
|||||||
leave(roomId, options = null) {
|
leave(roomId, options = null) {
|
||||||
return this._post(`/rooms/${encodeURIComponent(roomId)}/leave`, null, null, options);
|
return this._post(`/rooms/${encodeURIComponent(roomId)}/leave`, null, null, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
forget(roomId, options = null) {
|
||||||
|
return this._post(`/rooms/${encodeURIComponent(roomId)}/forget`, null, null, options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
import {Request as MockRequest} from "../../mocks/Request.js";
|
import {Request as MockRequest} from "../../mocks/Request.js";
|
||||||
|
@ -24,6 +24,7 @@ export class ArchivedRoom extends BaseRoom {
|
|||||||
// archived rooms are reference counted,
|
// archived rooms are reference counted,
|
||||||
// as they are not kept in memory when not needed
|
// as they are not kept in memory when not needed
|
||||||
this._releaseCallback = options.releaseCallback;
|
this._releaseCallback = options.releaseCallback;
|
||||||
|
this._forgetCallback = options.forgetCallback;
|
||||||
this._retentionCount = 1;
|
this._retentionCount = 1;
|
||||||
/**
|
/**
|
||||||
Some details from our own member event when being kicked or banned.
|
Some details from our own member event when being kicked or banned.
|
||||||
@ -126,8 +127,40 @@ export class ArchivedRoom extends BaseRoom {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
forget() {
|
forget(log = null) {
|
||||||
|
return this._platform.logger.wrapOrRun(log, "forget room", async log => {
|
||||||
|
log.set("id", this.id);
|
||||||
|
await this._hsApi.forget(this.id, {log}).response();
|
||||||
|
const storeNames = this._storage.storeNames;
|
||||||
|
const txn = await this._storage.readWriteTxn([
|
||||||
|
storeNames.roomState,
|
||||||
|
storeNames.archivedRoomSummary,
|
||||||
|
storeNames.roomMembers,
|
||||||
|
storeNames.timelineEvents,
|
||||||
|
storeNames.timelineFragments,
|
||||||
|
storeNames.pendingEvents,
|
||||||
|
storeNames.inboundGroupSessions,
|
||||||
|
storeNames.groupSessionDecryptions,
|
||||||
|
storeNames.operations,
|
||||||
|
]);
|
||||||
|
|
||||||
|
txn.roomState.removeAllForRoom(this.id);
|
||||||
|
txn.archivedRoomSummary.remove(this.id);
|
||||||
|
txn.roomMembers.removeAllForRoom(this.id);
|
||||||
|
txn.timelineEvents.removeAllForRoom(this.id);
|
||||||
|
txn.timelineFragments.removeAllForRoom(this.id);
|
||||||
|
txn.pendingEvents.removeAllForRoom(this.id);
|
||||||
|
txn.inboundGroupSessions.removeAllForRoom(this.id);
|
||||||
|
txn.groupSessionDecryptions.removeAllForRoom(this.id);
|
||||||
|
await txn.operations.removeAllForScope(this.id);
|
||||||
|
|
||||||
|
await txn.complete();
|
||||||
|
|
||||||
|
this._retentionCount = 0;
|
||||||
|
this._releaseCallback();
|
||||||
|
|
||||||
|
this._forgetCallback(this.id);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -367,6 +367,13 @@ export class Room extends BaseRoom {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
leave(log = null) {
|
||||||
|
return this._platform.logger.wrapOrRun(log, "leave room", async log => {
|
||||||
|
log.set("id", this.id);
|
||||||
|
await this._hsApi.leave(this.id, {log}).response();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/* called by BaseRoom to pass pendingEvents when opening the timeline */
|
/* called by BaseRoom to pass pendingEvents when opening the timeline */
|
||||||
_getPendingEvents() {
|
_getPendingEvents() {
|
||||||
return this._sendQueue.pendingEvents;
|
return this._sendQueue.pendingEvents;
|
||||||
|
@ -42,6 +42,16 @@ export class RoomStatus {
|
|||||||
return RoomStatus.none;
|
return RoomStatus.none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
withoutArchived() {
|
||||||
|
if (!this.archived) {
|
||||||
|
return this;
|
||||||
|
} else if (this.invited) {
|
||||||
|
return RoomStatus.invited;
|
||||||
|
} else {
|
||||||
|
return RoomStatus.none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RoomStatus.joined = new RoomStatus(true, false, false);
|
RoomStatus.joined = new RoomStatus(true, false, false);
|
||||||
|
@ -113,10 +113,18 @@ export class QueryTarget {
|
|||||||
return maxKey;
|
return maxKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async iterateValues(range, callback) {
|
||||||
|
const cursor = this._target.openCursor(range, "next");
|
||||||
|
await iterateCursor(cursor, (value, key, cur) => {
|
||||||
|
return {done: callback(value, key, cur)};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async iterateKeys(range, callback) {
|
async iterateKeys(range, callback) {
|
||||||
const cursor = this._target.openKeyCursor(range, "next");
|
const cursor = this._target.openKeyCursor(range, "next");
|
||||||
await iterateCursor(cursor, (_, key) => {
|
await iterateCursor(cursor, (_, key, cur) => {
|
||||||
return {done: callback(key)};
|
return {done: callback(key, cur)};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import {iterateCursor, reqAsPromise} from "./utils.js";
|
|||||||
import {RoomMember, EVENT_TYPE as MEMBER_EVENT_TYPE} from "../../room/members/RoomMember.js";
|
import {RoomMember, EVENT_TYPE as MEMBER_EVENT_TYPE} from "../../room/members/RoomMember.js";
|
||||||
import {RoomMemberStore} from "./stores/RoomMemberStore.js";
|
import {RoomMemberStore} from "./stores/RoomMemberStore.js";
|
||||||
import {SessionStore} from "./stores/SessionStore.js";
|
import {SessionStore} from "./stores/SessionStore.js";
|
||||||
|
import {encodeScopeTypeKey} from "./stores/OperationStore.js";
|
||||||
|
|
||||||
// FUNCTIONS SHOULD ONLY BE APPENDED!!
|
// FUNCTIONS SHOULD ONLY BE APPENDED!!
|
||||||
// the index in the array is the database version
|
// the index in the array is the database version
|
||||||
@ -14,6 +15,7 @@ export const schema = [
|
|||||||
createAccountDataStore,
|
createAccountDataStore,
|
||||||
createInviteStore,
|
createInviteStore,
|
||||||
createArchivedRoomSummaryStore,
|
createArchivedRoomSummaryStore,
|
||||||
|
migrateOperationScopeIndex,
|
||||||
];
|
];
|
||||||
// TODO: how to deal with git merge conflicts of this array?
|
// TODO: how to deal with git merge conflicts of this array?
|
||||||
|
|
||||||
@ -115,3 +117,22 @@ function createInviteStore(db) {
|
|||||||
function createArchivedRoomSummaryStore(db) {
|
function createArchivedRoomSummaryStore(db) {
|
||||||
db.createObjectStore("archivedRoomSummary", {keyPath: "summary.roomId"});
|
db.createObjectStore("archivedRoomSummary", {keyPath: "summary.roomId"});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// v9
|
||||||
|
async function migrateOperationScopeIndex(db, txn) {
|
||||||
|
try {
|
||||||
|
const operations = txn.objectStore("operations");
|
||||||
|
operations.deleteIndex("byTypeAndScope");
|
||||||
|
await iterateCursor(operations.openCursor(), (op, key, cur) => {
|
||||||
|
const {typeScopeKey} = op;
|
||||||
|
delete op.typeScopeKey;
|
||||||
|
const [type, scope] = typeScopeKey.split("|");
|
||||||
|
op.scopeTypeKey = encodeScopeTypeKey(scope, type);
|
||||||
|
cur.update(op);
|
||||||
|
});
|
||||||
|
operations.createIndex("byScopeAndType", "scopeTypeKey", {unique: false});
|
||||||
|
} catch (err) {
|
||||||
|
txn.abort();
|
||||||
|
console.error("could not migrate operations", err.stack);
|
||||||
|
}
|
||||||
|
}
|
@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {MIN_UNICODE, MAX_UNICODE} from "./common.js";
|
||||||
|
|
||||||
function encodeKey(roomId, sessionId, messageIndex) {
|
function encodeKey(roomId, sessionId, messageIndex) {
|
||||||
return `${roomId}|${sessionId}|${messageIndex}`;
|
return `${roomId}|${sessionId}|${messageIndex}`;
|
||||||
}
|
}
|
||||||
@ -31,4 +33,12 @@ export class GroupSessionDecryptionStore {
|
|||||||
decryption.key = encodeKey(roomId, sessionId, messageIndex);
|
decryption.key = encodeKey(roomId, sessionId, messageIndex);
|
||||||
this._store.put(decryption);
|
this._store.put(decryption);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeAllForRoom(roomId) {
|
||||||
|
const range = IDBKeyRange.bound(
|
||||||
|
encodeKey(roomId, MIN_UNICODE, MIN_UNICODE),
|
||||||
|
encodeKey(roomId, MAX_UNICODE, MAX_UNICODE)
|
||||||
|
);
|
||||||
|
this._store.delete(range);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {MIN_UNICODE, MAX_UNICODE} from "./common.js";
|
||||||
|
|
||||||
function encodeKey(roomId, senderKey, sessionId) {
|
function encodeKey(roomId, senderKey, sessionId) {
|
||||||
return `${roomId}|${senderKey}|${sessionId}`;
|
return `${roomId}|${senderKey}|${sessionId}`;
|
||||||
}
|
}
|
||||||
@ -37,4 +39,12 @@ export class InboundGroupSessionStore {
|
|||||||
session.key = encodeKey(session.roomId, session.senderKey, session.sessionId);
|
session.key = encodeKey(session.roomId, session.senderKey, session.sessionId);
|
||||||
this._store.put(session);
|
this._store.put(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeAllForRoom(roomId) {
|
||||||
|
const range = IDBKeyRange.bound(
|
||||||
|
encodeKey(roomId, MIN_UNICODE, MIN_UNICODE),
|
||||||
|
encodeKey(roomId, MAX_UNICODE, MAX_UNICODE)
|
||||||
|
);
|
||||||
|
this._store.delete(range);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,9 +13,10 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
import {MIN_UNICODE, MAX_UNICODE} from "./common.js";
|
||||||
|
|
||||||
function encodeTypeScopeKey(type, scope) {
|
export function encodeScopeTypeKey(scope, type) {
|
||||||
return `${type}|${scope}`;
|
return `${scope}|${type}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class OperationStore {
|
export class OperationStore {
|
||||||
@ -28,10 +29,10 @@ export class OperationStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getAllByTypeAndScope(type, scope) {
|
async getAllByTypeAndScope(type, scope) {
|
||||||
const key = encodeTypeScopeKey(type, scope);
|
const key = encodeScopeTypeKey(scope, type);
|
||||||
const results = [];
|
const results = [];
|
||||||
await this._store.index("byTypeAndScope").iterateWhile(key, value => {
|
await this._store.index("byScopeAndType").iterateWhile(key, value => {
|
||||||
if (value.typeScopeKey !== key) {
|
if (value.scopeTypeKey !== key) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
results.push(value);
|
results.push(value);
|
||||||
@ -41,7 +42,7 @@ export class OperationStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
add(operation) {
|
add(operation) {
|
||||||
operation.typeScopeKey = encodeTypeScopeKey(operation.type, operation.scope);
|
operation.scopeTypeKey = encodeScopeTypeKey(operation.scope, operation.type);
|
||||||
this._store.add(operation);
|
this._store.add(operation);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,4 +53,16 @@ export class OperationStore {
|
|||||||
remove(id) {
|
remove(id) {
|
||||||
this._store.delete(id);
|
this._store.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async removeAllForScope(scope) {
|
||||||
|
const range = IDBKeyRange.bound(
|
||||||
|
encodeScopeTypeKey(scope, MIN_UNICODE),
|
||||||
|
encodeScopeTypeKey(scope, MAX_UNICODE)
|
||||||
|
);
|
||||||
|
const index = this._store.index("byScopeAndType");
|
||||||
|
await index.iterateValues(range, (_, __, cur) => {
|
||||||
|
cur.delete();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,4 +68,11 @@ export class PendingEventStore {
|
|||||||
getAll() {
|
getAll() {
|
||||||
return this._eventStore.selectAll();
|
return this._eventStore.selectAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeAllForRoom(roomId) {
|
||||||
|
const minKey = encodeKey(roomId, KeyLimits.minStorageKey);
|
||||||
|
const maxKey = encodeKey(roomId, KeyLimits.maxStorageKey);
|
||||||
|
const range = IDBKeyRange.bound(minKey, maxKey);
|
||||||
|
this._eventStore.delete(range);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -257,4 +257,11 @@ export class TimelineEventStore {
|
|||||||
getByEventId(roomId, eventId) {
|
getByEventId(roomId, eventId) {
|
||||||
return this._timelineStore.index("byEventId").get(encodeEventIdKey(roomId, eventId));
|
return this._timelineStore.index("byEventId").get(encodeEventIdKey(roomId, eventId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeAllForRoom(roomId) {
|
||||||
|
const minKey = encodeKey(roomId, KeyLimits.minStorageKey, KeyLimits.minStorageKey);
|
||||||
|
const maxKey = encodeKey(roomId, KeyLimits.maxStorageKey, KeyLimits.maxStorageKey);
|
||||||
|
const range = IDBKeyRange.bound(minKey, maxKey);
|
||||||
|
this._timelineStore.delete(range);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,4 +72,8 @@ export class TimelineFragmentStore {
|
|||||||
get(roomId, fragmentId) {
|
get(roomId, fragmentId) {
|
||||||
return this._store.get(encodeKey(roomId, fragmentId));
|
return this._store.get(encodeKey(roomId, fragmentId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeAllForRoom(roomId) {
|
||||||
|
this._store.delete(this._allRange(roomId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -443,6 +443,11 @@ a {
|
|||||||
font-size: 14rem;
|
font-size: 14rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.RoomHeader .room-options {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.RoomView_error {
|
.RoomView_error {
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,14 @@ export class Popup {
|
|||||||
this._trackingTemplateView.addSubView(this);
|
this._trackingTemplateView.addSubView(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@param {DOMElement}
|
||||||
|
@param {string} arrangement.relativeTo: whether top/left or bottom/right is used to position
|
||||||
|
@param {string} arrangement.align: how much of the popup axis size (start: 0, end: width or center: width/2)
|
||||||
|
is taken into account when positioning relative to the target
|
||||||
|
@param {number} arrangement.before extra padding to shift the final positioning with
|
||||||
|
@param {number} arrangement.after extra padding to shift the final positioning with
|
||||||
|
*/
|
||||||
showRelativeTo(target, arrangement) {
|
showRelativeTo(target, arrangement) {
|
||||||
this._target = target;
|
this._target = target;
|
||||||
this._arrangement = arrangement;
|
this._arrangement = arrangement;
|
||||||
@ -102,6 +110,9 @@ export class Popup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_applyArrangementAxis(axis, {relativeTo, align, before, after}) {
|
_applyArrangementAxis(axis, {relativeTo, align, before, after}) {
|
||||||
|
// TODO: using {relativeTo: "end", align: "start"} to align the right edge of the popup
|
||||||
|
// with the right side of the target doens't make sense here, we'd expect align: "right"?
|
||||||
|
// see RoomView
|
||||||
if (relativeTo === "end") {
|
if (relativeTo === "end") {
|
||||||
let end = axis.size(this._target.offsetParent) - axis.offsetStart(this._target);
|
let end = axis.size(this._target.offsetParent) - axis.offsetStart(this._target);
|
||||||
if (align === "end") {
|
if (align === "end") {
|
||||||
|
@ -16,6 +16,8 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {TemplateView} from "../../general/TemplateView.js";
|
import {TemplateView} from "../../general/TemplateView.js";
|
||||||
|
import {Popup} from "../../general/Popup.js";
|
||||||
|
import {Menu} from "../../general/Menu.js";
|
||||||
import {TimelineList} from "./TimelineList.js";
|
import {TimelineList} from "./TimelineList.js";
|
||||||
import {TimelineLoadingView} from "./TimelineLoadingView.js";
|
import {TimelineLoadingView} from "./TimelineLoadingView.js";
|
||||||
import {MessageComposer} from "./MessageComposer.js";
|
import {MessageComposer} from "./MessageComposer.js";
|
||||||
@ -23,6 +25,11 @@ import {RoomArchivedView} from "./RoomArchivedView.js";
|
|||||||
import {AvatarView} from "../../avatar.js";
|
import {AvatarView} from "../../avatar.js";
|
||||||
|
|
||||||
export class RoomView extends TemplateView {
|
export class RoomView extends TemplateView {
|
||||||
|
constructor(options) {
|
||||||
|
super(options);
|
||||||
|
this._optionsPopup = null;
|
||||||
|
}
|
||||||
|
|
||||||
render(t, vm) {
|
render(t, vm) {
|
||||||
let bottomView;
|
let bottomView;
|
||||||
if (vm.composerViewModel.kind === "composer") {
|
if (vm.composerViewModel.kind === "composer") {
|
||||||
@ -37,6 +44,10 @@ export class RoomView extends TemplateView {
|
|||||||
t.div({className: "room-description"}, [
|
t.div({className: "room-description"}, [
|
||||||
t.h2(vm => vm.name),
|
t.h2(vm => vm.name),
|
||||||
]),
|
]),
|
||||||
|
t.button({
|
||||||
|
className: "button-utility room-options",
|
||||||
|
onClick: evt => this._toggleOptionsMenu(evt)
|
||||||
|
}, "⋮")
|
||||||
]),
|
]),
|
||||||
t.div({className: "RoomView_body"}, [
|
t.div({className: "RoomView_body"}, [
|
||||||
t.div({className: "RoomView_error"}, vm => vm.error),
|
t.div({className: "RoomView_error"}, vm => vm.error),
|
||||||
@ -49,4 +60,36 @@ export class RoomView extends TemplateView {
|
|||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_toggleOptionsMenu(evt) {
|
||||||
|
if (this._optionsPopup && this._optionsPopup.isOpen) {
|
||||||
|
this._optionsPopup.close();
|
||||||
|
} else {
|
||||||
|
const vm = this.value;
|
||||||
|
const options = [];
|
||||||
|
if (vm.canLeave) {
|
||||||
|
options.push(Menu.option(vm.i18n`Leave room`, () => vm.leaveRoom()));
|
||||||
|
}
|
||||||
|
if (vm.canForget) {
|
||||||
|
options.push(Menu.option(vm.i18n`Forget room`, () => vm.forgetRoom()));
|
||||||
|
}
|
||||||
|
if (!options.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._optionsPopup = new Popup(new Menu(options));
|
||||||
|
this._optionsPopup.trackInTemplateView(this);
|
||||||
|
this._optionsPopup.showRelativeTo(evt.target, {
|
||||||
|
horizontal: {
|
||||||
|
relativeTo: "end",
|
||||||
|
align: "start",
|
||||||
|
after: 0
|
||||||
|
},
|
||||||
|
vertical: {
|
||||||
|
relativeTo: "start",
|
||||||
|
align: "start",
|
||||||
|
after: 40 + 4
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user