Add rooms & pending invites to client page, with the options to leave a room and accept, ignore or reject an invite
This commit is contained in:
parent
3e8e034a5a
commit
007fcb1c02
29
alembic/versions/fcb4ea0fce29_create_invite_table.py
Normal file
29
alembic/versions/fcb4ea0fce29_create_invite_table.py
Normal file
@ -0,0 +1,29 @@
|
||||
"""create invite table
|
||||
|
||||
Revision ID: fcb4ea0fce29
|
||||
Revises: 90aa88820eab
|
||||
Create Date: 2022-01-18 02:16:53.954662
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'fcb4ea0fce29'
|
||||
down_revision = '90aa88820eab'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table('invite',
|
||||
sa.Column('client', sa.String(255), sa.ForeignKey("client.id", onupdate="CASCADE", ondelete="CASCADE"), primary_key=True),
|
||||
sa.Column('room', sa.String(255), nullable=False, primary_key=True),
|
||||
sa.Column('date', sa.DateTime(), nullable=False),
|
||||
sa.Column('inviter', sa.String(255), nullable=False)
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table('invite')
|
@ -13,21 +13,22 @@
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
from typing import Dict, Iterable, Optional, Set, Callable, Any, Awaitable, Union, TYPE_CHECKING
|
||||
from typing import Dict, Iterable, List, Optional, Set, Callable, Any, Awaitable, Union, TYPE_CHECKING
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from aiohttp import ClientSession
|
||||
|
||||
from mautrix.errors import MatrixInvalidToken
|
||||
from mautrix.types import (UserID, SyncToken, FilterID, ContentURI, StrippedStateEvent, Membership,
|
||||
StateEvent, EventType, Filter, RoomFilter, RoomEventFilter, EventFilter,
|
||||
PresenceState, StateFilter, DeviceID)
|
||||
PresenceState, StateFilter, DeviceID, RoomID)
|
||||
from mautrix.client import InternalEventType
|
||||
from mautrix.client.state_store.sqlalchemy import SQLStateStore as BaseSQLStateStore
|
||||
|
||||
from .lib.store_proxy import SyncStoreProxy
|
||||
from .db import DBClient
|
||||
from .db import DBClient, DBInvite
|
||||
from .matrix import MaubotMatrixClient
|
||||
|
||||
try:
|
||||
@ -65,9 +66,11 @@ class Client:
|
||||
|
||||
remote_displayname: Optional[str]
|
||||
remote_avatar_url: Optional[ContentURI]
|
||||
remote_rooms: Optional[List[RoomID]]
|
||||
|
||||
def __init__(self, db_instance: DBClient) -> None:
|
||||
self.db_instance = db_instance
|
||||
self.db_invites = DBInvite.get(self.id)
|
||||
self.cache[self.id] = self
|
||||
self.log = log.getChild(self.id)
|
||||
self.references = set()
|
||||
@ -75,6 +78,7 @@ class Client:
|
||||
self.sync_ok = True
|
||||
self.remote_displayname = None
|
||||
self.remote_avatar_url = None
|
||||
self.remote_rooms = None
|
||||
self.client = MaubotMatrixClient(mxid=self.id, base_url=self.homeserver,
|
||||
token=self.access_token, client_session=self.http_client,
|
||||
log=self.log, loop=self.loop, device_id=self.device_id,
|
||||
@ -88,7 +92,6 @@ class Client:
|
||||
self.client.ignore_initial_sync = True
|
||||
self.client.ignore_first_sync = True
|
||||
self.client.presence = PresenceState.ONLINE if self.online else PresenceState.OFFLINE
|
||||
if self.autojoin:
|
||||
self.client.add_event_handler(EventType.ROOM_MEMBER, self._handle_invite)
|
||||
self.client.add_event_handler(EventType.ROOM_TOMBSTONE, self._handle_tombstone)
|
||||
self.client.add_event_handler(InternalEventType.SYNC_ERRORED, self._set_sync_ok(False))
|
||||
@ -253,6 +256,13 @@ class Client:
|
||||
"avatar_url": self.avatar_url,
|
||||
"remote_displayname": self.remote_displayname,
|
||||
"remote_avatar_url": self.remote_avatar_url,
|
||||
"invites": [{
|
||||
"client": i.client,
|
||||
"room": i.room,
|
||||
"date": i.date.timestamp(),
|
||||
"inviter": i.inviter
|
||||
} for i in self.invites],
|
||||
"rooms": self.remote_rooms,
|
||||
"instances": [instance.to_dict() for instance in self.references],
|
||||
}
|
||||
|
||||
@ -275,11 +285,23 @@ class Client:
|
||||
self.log.info(f"{evt.room_id} tombstoned with no replacement, ignoring")
|
||||
return
|
||||
_, server = self.client.parse_user_id(evt.sender)
|
||||
DBInvite.update_tombstone(user.id, evt.room_id, evt.content.replacement_room)
|
||||
await self.client.join_room(evt.content.replacement_room, servers=[server])
|
||||
|
||||
async def _handle_invite(self, evt: StrippedStateEvent) -> None:
|
||||
if evt.state_key == self.id and evt.content.membership == Membership.INVITE:
|
||||
if evt.state_key != self.id or evt.content.membership != Membership.INVITE:
|
||||
return
|
||||
if self.autojoin:
|
||||
await self.client.join_room(evt.room_id)
|
||||
await self._update_remote_profile()
|
||||
else:
|
||||
self.log.debug('Inserting invite into database for later handling')
|
||||
DBInvite(
|
||||
client=self.id,
|
||||
room=evt.room_id,
|
||||
date=datetime.fromtimestamp(evt.timestamp//1000),
|
||||
inviter=evt.sender
|
||||
).upsert()
|
||||
|
||||
async def update_started(self, started: bool) -> None:
|
||||
if started is None or started == self.started:
|
||||
@ -307,6 +329,26 @@ class Client:
|
||||
else:
|
||||
await self._update_remote_profile()
|
||||
|
||||
async def join_room(self, room: RoomID) -> None:
|
||||
if room is None:
|
||||
return
|
||||
await self.client.join_room(room)
|
||||
DBInvite(client=self.id, room=room).delete()
|
||||
await self._update_remote_profile()
|
||||
|
||||
async def leave_room(self, room: RoomID) -> None:
|
||||
if room is None:
|
||||
return
|
||||
await self.client.leave_room(room)
|
||||
DBInvite(client=self.id, room=room).delete()
|
||||
await self._update_remote_profile()
|
||||
|
||||
async def ignore_invite(self, room: RoomID) -> None:
|
||||
if room is None:
|
||||
return
|
||||
DBInvite(client=self.id, room=room).delete()
|
||||
await self._update_remote_profile()
|
||||
|
||||
async def update_access_details(self, access_token: Optional[str], homeserver: Optional[str],
|
||||
device_id: Optional[str] = None) -> None:
|
||||
if not access_token and not homeserver:
|
||||
@ -354,6 +396,7 @@ class Client:
|
||||
async def _update_remote_profile(self) -> None:
|
||||
profile = await self.client.get_profile(self.id)
|
||||
self.remote_displayname, self.remote_avatar_url = profile.displayname, profile.avatar_url
|
||||
self.remote_rooms = await self.client.get_joined_rooms()
|
||||
|
||||
# region Properties
|
||||
|
||||
@ -412,12 +455,12 @@ class Client:
|
||||
def autojoin(self, value: bool) -> None:
|
||||
if value == self.db_instance.autojoin:
|
||||
return
|
||||
if value:
|
||||
self.client.add_event_handler(EventType.ROOM_MEMBER, self._handle_invite)
|
||||
else:
|
||||
self.client.remove_event_handler(EventType.ROOM_MEMBER, self._handle_invite)
|
||||
self.db_instance.autojoin = value
|
||||
|
||||
@property
|
||||
def invites(self) -> List[DBInvite]:
|
||||
return DBInvite.get(self.id)
|
||||
|
||||
@property
|
||||
def online(self) -> bool:
|
||||
return self.db_instance.online
|
||||
|
38
maubot/db.py
38
maubot/db.py
@ -16,12 +16,13 @@
|
||||
from typing import Iterable, Optional
|
||||
import logging
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Column, String, Boolean, ForeignKey, Text
|
||||
from sqlalchemy import Column, String, Boolean, ForeignKey, Text, DateTime
|
||||
from sqlalchemy.engine.base import Engine
|
||||
import sqlalchemy as sql
|
||||
|
||||
from mautrix.types import UserID, FilterID, DeviceID, SyncToken, ContentURI
|
||||
from mautrix.types import UserID, RoomID, FilterID, DeviceID, SyncToken, ContentURI
|
||||
from mautrix.util.db import Base
|
||||
from mautrix.client.state_store.sqlalchemy import RoomState, UserProfile
|
||||
|
||||
@ -76,11 +77,42 @@ class DBClient(Base):
|
||||
return cls._select_one_or_none(cls.c.id == id)
|
||||
|
||||
|
||||
class DBInvite(Base):
|
||||
__tablename__ = "invite"
|
||||
|
||||
client: UserID = Column(String(255), ForeignKey("client.id", onupdate="CASCADE", ondelete="CASCADE"), primary_key=True)
|
||||
room: RoomID = Column(String(255), nullable=False, primary_key=True)
|
||||
date: datetime = Column(DateTime(), nullable=False)
|
||||
inviter: UserID = Column(String(255), nullable=False)
|
||||
|
||||
@classmethod
|
||||
def all(cls) -> Iterable['DBInvite']:
|
||||
return cls._select_all()
|
||||
|
||||
@classmethod
|
||||
def get(cls, client: UserID, room: RoomID) -> Optional['DBInvite']:
|
||||
return cls._select_one_or_none(cls.c.client == client, cls.c.room == room)
|
||||
|
||||
@classmethod
|
||||
def get(cls, client: UserID) -> Iterable['DBInvite']:
|
||||
return cls._select_all(cls.c.client == client)
|
||||
|
||||
@classmethod
|
||||
def update_tombstone(cls, client: UserID, old: RoomID, new: RoomID) -> Optional['DBInvite']:
|
||||
invite = cls.get(client=client, room=old)
|
||||
if not invite:
|
||||
return None
|
||||
invite.delete()
|
||||
invite.room = new
|
||||
invite.insert()
|
||||
return invite
|
||||
|
||||
|
||||
def init(config: Config) -> Engine:
|
||||
db = sql.create_engine(config["database"])
|
||||
Base.metadata.bind = db
|
||||
|
||||
for table in (DBPlugin, DBClient, RoomState, UserProfile):
|
||||
for table in (DBPlugin, DBClient, RoomState, UserProfile, DBInvite):
|
||||
table.bind(db)
|
||||
|
||||
if not db.has_table("alembic_version"):
|
||||
|
@ -89,11 +89,7 @@ async def _update_client(client: Client, data: dict, is_login: bool = False) ->
|
||||
except MatrixConnectionError:
|
||||
return resp.bad_client_connection_details
|
||||
except ValueError as e:
|
||||
str_err = str(e)
|
||||
if str_err.startswith("MXID mismatch"):
|
||||
return resp.mxid_mismatch(str(e)[len("MXID mismatch: "):])
|
||||
elif str_err.startswith("Device ID mismatch"):
|
||||
return resp.device_id_mismatch(str(e)[len("Device ID mismatch: "):])
|
||||
with client.db_instance.edit_mode():
|
||||
await client.update_avatar_url(data.get("avatar_url", None))
|
||||
await client.update_displayname(data.get("displayname", None))
|
||||
@ -155,3 +151,30 @@ async def clear_client_cache(request: web.Request) -> web.Response:
|
||||
return resp.client_not_found
|
||||
client.clear_cache()
|
||||
return resp.ok
|
||||
|
||||
|
||||
@routes.post("/client/{id}/room/{room}/join")
|
||||
async def join_room(request: web.Request) -> web.Response:
|
||||
user_id = request.match_info.get("id", None)
|
||||
room_id = request.match_info.get("room", None)
|
||||
client = Client.get(user_id, None)
|
||||
await client.join_room(room_id)
|
||||
return resp.updated(client.to_dict(), is_login=False)
|
||||
|
||||
|
||||
@routes.post("/client/{id}/room/{room}/leave")
|
||||
async def leave_room(request: web.Request) -> web.Response:
|
||||
user_id = request.match_info.get("id", None)
|
||||
room_id = request.match_info.get("room", None)
|
||||
client = Client.get(user_id, None)
|
||||
await client.leave_room(room_id)
|
||||
return resp.updated(client.to_dict(), is_login=False)
|
||||
|
||||
|
||||
@routes.post("/client/{id}/room/{room}/ignore")
|
||||
async def leave_room(request: web.Request) -> web.Response:
|
||||
user_id = request.match_info.get("id", None)
|
||||
room_id = request.match_info.get("room", None)
|
||||
client = Client.get(user_id, None)
|
||||
await client.ignore_invite(room_id)
|
||||
return resp.updated(client.to_dict(), is_login=False)
|
||||
|
@ -425,6 +425,93 @@ paths:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
404:
|
||||
$ref: '#/components/responses/ClientNotFound'
|
||||
'/client/{id}/room/{room}/join':
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
description: The Matrix user ID of the client to join a room with
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: room
|
||||
in: path
|
||||
description: The Matrix room to join
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
post:
|
||||
operationId: join_room
|
||||
summary: Join a Matrix room or accept an invite
|
||||
tags: [Clients]
|
||||
responses:
|
||||
200:
|
||||
description: Joined the room
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MatrixClient'
|
||||
401:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
404:
|
||||
$ref: '#/components/responses/ClientNotFound'
|
||||
'/client/{id}/room/{room}/leave':
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
description: The Matrix user ID of the client to leave a room with
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: id
|
||||
in: path
|
||||
description: The Matrix room to leave or reject
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
post:
|
||||
operationId: leave_room
|
||||
summary: Leave a Matrix room or reject an invite
|
||||
tags: [Clients]
|
||||
responses:
|
||||
200:
|
||||
description: Left the room
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MatrixClient'
|
||||
401:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
404:
|
||||
$ref: '#/components/responses/ClientNotFound'
|
||||
'/client/{id}/room/{room}/ignore':
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
description: The Matrix user ID of the client to remove the invite from
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: id
|
||||
in: path
|
||||
description: The Matrix room whose invite to remove
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
post:
|
||||
operationId: ignore_invite
|
||||
summary: Ignore a room invitation (do not answer it, but remove from the database)
|
||||
tags: [Clients]
|
||||
responses:
|
||||
200:
|
||||
description: Removed the invitation.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MatrixClient'
|
||||
401:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
404:
|
||||
$ref: '#/components/responses/ClientNotFound'
|
||||
/client/auth/servers:
|
||||
get:
|
||||
operationId: get_client_auth_servers
|
||||
@ -710,6 +797,36 @@ components:
|
||||
type: string
|
||||
example: 'mxc://maunium.net/FsPQQTntCCqhJMFtwArmJdaU'
|
||||
description: The content URI of the avatar for this client.
|
||||
rooms:
|
||||
type: array
|
||||
readOnly: true
|
||||
description: List of room IDs of currently joined room.
|
||||
items:
|
||||
type: string
|
||||
example: '!foobar:example.org'
|
||||
invites:
|
||||
type: array
|
||||
readOnly: true
|
||||
description: List of pending invites.
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
client:
|
||||
type: string
|
||||
example: '@foobar:example.org'
|
||||
description: The client that was invited to a room.
|
||||
room:
|
||||
type: string
|
||||
example: '!foobar:example.org'
|
||||
description: The room the client was invited to.
|
||||
date:
|
||||
type: integer
|
||||
example: 1642546523
|
||||
description: UNIX timestamp when the invite was created.
|
||||
inviter:
|
||||
type: string
|
||||
example: '@bar:example.org'
|
||||
description: The user who invited this client to the room.
|
||||
instances:
|
||||
type: array
|
||||
readOnly: true
|
||||
|
@ -233,6 +233,30 @@ export async function clearClientCache(id) {
|
||||
return await resp.json()
|
||||
}
|
||||
|
||||
export async function joinRoom(id, room) {
|
||||
const resp = await fetch(`${BASE_PATH}/client/${id}/room/${room}/join`, {
|
||||
headers: getHeaders(),
|
||||
method: "POST",
|
||||
})
|
||||
return await resp.json()
|
||||
}
|
||||
|
||||
export async function leaveRoom(id, room) {
|
||||
const resp = await fetch(`${BASE_PATH}/client/${id}/room/${room}/leave`, {
|
||||
headers: getHeaders(),
|
||||
method: "POST",
|
||||
})
|
||||
return await resp.json()
|
||||
}
|
||||
|
||||
export async function ignoreInvite(id, room) {
|
||||
const resp = await fetch(`${BASE_PATH}/client/${id}/room/${room}/ignore`, {
|
||||
headers: getHeaders(),
|
||||
method: "POST",
|
||||
})
|
||||
return await resp.json()
|
||||
}
|
||||
|
||||
export const getClientAuthServers = () => defaultGet("/client/auth/servers")
|
||||
|
||||
export async function doClientAuth(server, type, username, password) {
|
||||
@ -253,5 +277,5 @@ export default {
|
||||
getInstanceDatabase, queryInstanceDatabase,
|
||||
getPlugins, getPlugin, uploadPlugin, deletePlugin,
|
||||
getClients, getClient, uploadAvatar, getAvatarURL, putClient, deleteClient, clearClientCache,
|
||||
getClientAuthServers, doClientAuth,
|
||||
joinRoom, leaveRoom, ignoreInvite, getClientAuthServers, doClientAuth,
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ class Client extends BaseMainView {
|
||||
|
||||
get entryKeys() {
|
||||
return ["id", "displayname", "homeserver", "avatar_url", "access_token", "device_id",
|
||||
"sync", "autojoin", "online", "enabled", "started"]
|
||||
"sync", "autojoin", "online", "enabled", "started", "rooms", "invites"]
|
||||
}
|
||||
|
||||
get initialState() {
|
||||
@ -81,6 +81,11 @@ class Client extends BaseMainView {
|
||||
enabled: true,
|
||||
online: true,
|
||||
started: false,
|
||||
rooms: [],
|
||||
invites: [],
|
||||
joiningRooms: [],
|
||||
ignoringRooms: [],
|
||||
leavingRooms: [],
|
||||
|
||||
instances: [],
|
||||
|
||||
@ -102,6 +107,9 @@ class Client extends BaseMainView {
|
||||
delete client.clearingCache
|
||||
delete client.error
|
||||
delete client.instances
|
||||
delete client.joiningRooms
|
||||
delete client.ignoringRooms
|
||||
delete client.leavingRooms
|
||||
return client
|
||||
}
|
||||
|
||||
@ -188,6 +196,75 @@ class Client extends BaseMainView {
|
||||
}
|
||||
}
|
||||
|
||||
joinRoom = async room => {
|
||||
if (this.state.joiningRooms.includes(room)) {
|
||||
return
|
||||
}
|
||||
this.setState({
|
||||
joiningRooms: this.state.joiningRooms.concat([room]),
|
||||
})
|
||||
const resp = await api.joinRoom(this.state.id, room)
|
||||
if (resp.id) {
|
||||
this.setState({
|
||||
joiningRooms: this.state.joiningRooms.filter(jr => jr !== room),
|
||||
invites: resp.invites,
|
||||
rooms: resp.rooms,
|
||||
error: "",
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
joiningRooms: this.state.joiningRooms.filter(jr => jr !== room),
|
||||
error: resp.error,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
leaveRoom = async room => {
|
||||
if (this.state.leavingRooms.includes(room)) {
|
||||
return
|
||||
}
|
||||
this.setState({
|
||||
leavingRooms: this.state.leavingRooms.concat([room]),
|
||||
})
|
||||
const resp = await api.leaveRoom(this.state.id, room)
|
||||
if (resp.id) {
|
||||
this.setState({
|
||||
leavingRooms: this.state.leavingRooms.filter(jr => jr !== room),
|
||||
invites: resp.invites,
|
||||
rooms: resp.rooms,
|
||||
error: "",
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
leavingRooms: this.state.leavingRooms.filter(jr => jr !== room),
|
||||
error: resp.error,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ignoreInvite = async room => {
|
||||
if (this.state.ignoringRooms.includes(room)) {
|
||||
return
|
||||
}
|
||||
this.setState({
|
||||
ignoringRooms: this.state.ignoringRooms.concat([room]),
|
||||
})
|
||||
const resp = await api.ignoreInvite(this.state.id, room)
|
||||
if (resp.id) {
|
||||
this.setState({
|
||||
ignoringRooms: this.state.ignoringRooms.filter(jr => jr !== room),
|
||||
invites: resp.invites,
|
||||
rooms: resp.rooms,
|
||||
error: "",
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
ignoringRooms: this.state.ignoringRooms.filter(jr => jr !== room),
|
||||
error: resp.error,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
get loading() {
|
||||
return this.state.saving || this.state.startingOrStopping
|
||||
|| this.clearingCache || this.state.deleting
|
||||
@ -325,6 +402,54 @@ class Client extends BaseMainView {
|
||||
<div className="error">{this.state.error}</div>
|
||||
</>
|
||||
|
||||
renderRoomsAndInvites = () => <>
|
||||
<div className="rooms">
|
||||
<h3>Joined Rooms</h3>
|
||||
<table class="rooms-list">
|
||||
{this.state.rooms.map((room, i) =>
|
||||
<tr>
|
||||
<td>{room}</td>
|
||||
<td class="right">
|
||||
<button class="button error-color" onClick={ev => this.leaveRoom(room)}
|
||||
disabled={this.state.leavingRooms.includes(room)}
|
||||
title={"Leave this room"}>
|
||||
{this.state.leavingRooms.includes(room) ? <Spinner/> : "✗"}
|
||||
</button>
|
||||
</td>
|
||||
</tr>,
|
||||
)}
|
||||
</table>
|
||||
{this.state.rooms.length === 0 ? <span class="no-rooms">Not in any rooms.</span> : ""}
|
||||
<h3>Pending Invites</h3>
|
||||
<table class="rooms-list">
|
||||
{this.state.invites.map((invite, i) =>
|
||||
<tr>
|
||||
<td>{invite.room}</td>
|
||||
<td class="right">
|
||||
<button class="button main-color" title={"Accept this invite"}
|
||||
onClick={ev => this.joinRoom(invite.room)}
|
||||
disabled={this.state.joiningRooms.includes(invite.room)}>
|
||||
{this.state.joiningRooms.includes(invite.room) ? <Spinner/> : "✓"}
|
||||
</button>
|
||||
<button class="button warning-color" title={"Ignore this invite"}
|
||||
onClick={ev => this.ignoreInvite(invite.room)}
|
||||
disabled={this.state.ignoringRooms.includes(invite.room)}>
|
||||
{this.state.ignoringRooms.includes(invite.room) ? <Spinner/> : "🗑"}
|
||||
</button>
|
||||
<button class="button error-color" title={"Reject this invite"}
|
||||
onClick={ev => this.leaveRoom(invite.room)}
|
||||
disabled={this.state.leavingRooms.includes(invite.room)}>
|
||||
{this.state.leavingRooms.includes(invite.room) ? <Spinner/> : "✗"}
|
||||
</button>
|
||||
</td>
|
||||
</tr>,
|
||||
)}
|
||||
</table>
|
||||
{this.state.invites.length === 0 ?
|
||||
<span class="no-rooms">No pending invites.</span> : ""}
|
||||
</div>
|
||||
</>
|
||||
|
||||
render() {
|
||||
return <>
|
||||
<div className="client">
|
||||
@ -333,6 +458,7 @@ class Client extends BaseMainView {
|
||||
{this.renderPreferences()}
|
||||
{this.renderPrefButtons()}
|
||||
{this.renderInstances()}
|
||||
{this.renderRoomsAndInvites()}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
@ -50,12 +50,38 @@
|
||||
background-color: $background-dark !important
|
||||
color: $text-color
|
||||
|
||||
=error-color-button()
|
||||
background-color: $error-light
|
||||
color: $inverted-text-color
|
||||
&:hover:not(:disabled)
|
||||
background-color: $error
|
||||
|
||||
&:disabled.disabled-bg
|
||||
background-color: $background-dark !important
|
||||
color: $text-color
|
||||
|
||||
=warning-color-button()
|
||||
background-color: $warning
|
||||
color: $inverted-text-color
|
||||
&:hover:not(:disabled)
|
||||
background-color: $warning-dark
|
||||
|
||||
&:disabled.disabled-bg
|
||||
background-color: $background-dark !important
|
||||
color: $text-color
|
||||
|
||||
.button
|
||||
+button
|
||||
|
||||
&.main-color
|
||||
+main-color-button
|
||||
|
||||
&.error-color
|
||||
+error-color-button
|
||||
|
||||
&.warning-color
|
||||
+warning-color-button
|
||||
|
||||
=button-group()
|
||||
width: 100%
|
||||
display: flex
|
||||
|
@ -24,6 +24,7 @@ $error: #B71C1C
|
||||
$error-dark: #7F0000
|
||||
$error-light: #F05545
|
||||
$warning: orange
|
||||
$warning-dark: #bf7b00
|
||||
|
||||
$border-color: #DDD
|
||||
$text-color: #212121
|
||||
|
@ -26,6 +26,7 @@
|
||||
|
||||
@import pages/mixins/upload-container
|
||||
@import pages/mixins/instancelist
|
||||
@import pages/mixins/roomlist
|
||||
|
||||
@import pages/login
|
||||
@import pages/dashboard
|
||||
|
@ -36,6 +36,9 @@
|
||||
> div.instances
|
||||
+instancelist
|
||||
|
||||
> div.rooms
|
||||
+roomlist
|
||||
|
||||
input.fingerprint
|
||||
font-family: "Fira Code", monospace
|
||||
font-size: 0.8em
|
||||
|
@ -0,0 +1,37 @@
|
||||
=roomlist()
|
||||
margin: 1rem 0
|
||||
|
||||
> h3
|
||||
margin: .5rem
|
||||
width: 100%
|
||||
|
||||
> span.no-rooms
|
||||
margin: .5rem
|
||||
font-size: .875rem
|
||||
font-weight: lighter
|
||||
|
||||
> table.rooms-list
|
||||
width: 100%
|
||||
margin: .5rem
|
||||
border-collapse: collapse
|
||||
text-align: left
|
||||
|
||||
td.right
|
||||
display: flex
|
||||
justify-content: flex-end
|
||||
align-content: flex-end
|
||||
flex-direction: row
|
||||
display: flex
|
||||
|
||||
.button
|
||||
margin: .1rem
|
||||
height: 31px
|
||||
width: 48px
|
||||
|
||||
> .spinner
|
||||
+thick-spinner
|
||||
+white-spinner
|
||||
width: 16px
|
||||
|
||||
> tr
|
||||
border-top: 1px solid $border-color
|
Loading…
Reference in New Issue
Block a user