mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-20 10:33:41 -05:00
Merge branch 'main' into nexy7574/extended-profiles
This commit is contained in:
commit
fc54faf551
28 changed files with 260 additions and 101 deletions
|
@ -75,7 +75,7 @@ require (
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
maunium.net/go/mautrix v0.22.2-0.20241222121030-33b4e823c5e5 // indirect
|
maunium.net/go/mautrix v0.22.2-0.20250101160025-077716a4ec7c // indirect
|
||||||
mvdan.cc/xurls/v2 v2.5.0 // indirect
|
mvdan.cc/xurls/v2 v2.5.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -250,7 +250,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
maunium.net/go/mautrix v0.22.2-0.20241222121030-33b4e823c5e5 h1:3aZCApUF3wlboN1BcAX6u6MGhju4OpsmhyfvxdO6tO0=
|
maunium.net/go/mautrix v0.22.2-0.20250101160025-077716a4ec7c h1:rVm1DvtGXvjy46pKmO6HYPDLMT6Wd83D1jtUhbnuPoM=
|
||||||
maunium.net/go/mautrix v0.22.2-0.20241222121030-33b4e823c5e5/go.mod h1:1rhqwH34Rz54ZqzdQYkmNW6rQUymNeTdaLA4l9LK6AI=
|
maunium.net/go/mautrix v0.22.2-0.20250101160025-077716a4ec7c/go.mod h1:1rhqwH34Rz54ZqzdQYkmNW6rQUymNeTdaLA4l9LK6AI=
|
||||||
mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
|
mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
|
||||||
mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
|
mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -25,7 +25,7 @@ require (
|
||||||
golang.org/x/text v0.21.0
|
golang.org/x/text v0.21.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
maunium.net/go/mauflag v1.0.0
|
maunium.net/go/mauflag v1.0.0
|
||||||
maunium.net/go/mautrix v0.22.2-0.20241222121030-33b4e823c5e5
|
maunium.net/go/mautrix v0.22.2-0.20250101160025-077716a4ec7c
|
||||||
mvdan.cc/xurls/v2 v2.5.0
|
mvdan.cc/xurls/v2 v2.5.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -91,7 +91,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
|
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
|
||||||
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
|
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
|
||||||
maunium.net/go/mautrix v0.22.2-0.20241222121030-33b4e823c5e5 h1:3aZCApUF3wlboN1BcAX6u6MGhju4OpsmhyfvxdO6tO0=
|
maunium.net/go/mautrix v0.22.2-0.20250101160025-077716a4ec7c h1:rVm1DvtGXvjy46pKmO6HYPDLMT6Wd83D1jtUhbnuPoM=
|
||||||
maunium.net/go/mautrix v0.22.2-0.20241222121030-33b4e823c5e5/go.mod h1:1rhqwH34Rz54ZqzdQYkmNW6rQUymNeTdaLA4l9LK6AI=
|
maunium.net/go/mautrix v0.22.2-0.20250101160025-077716a4ec7c/go.mod h1:1rhqwH34Rz54ZqzdQYkmNW6rQUymNeTdaLA4l9LK6AI=
|
||||||
mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
|
mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
|
||||||
mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
|
mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
|
||||||
|
|
|
@ -21,7 +21,8 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
getRoomBaseQuery = `
|
getRoomBaseQuery = `
|
||||||
SELECT room_id, creation_content, tombstone_content, name, name_quality, avatar, explicit_avatar, topic, canonical_alias,
|
SELECT room_id, creation_content, tombstone_content, name, name_quality,
|
||||||
|
avatar, explicit_avatar, dm_user_id, topic, canonical_alias,
|
||||||
lazy_load_summary, encryption_event, has_member_list, preview_event_rowid, sorting_timestamp,
|
lazy_load_summary, encryption_event, has_member_list, preview_event_rowid, sorting_timestamp,
|
||||||
unread_highlights, unread_notifications, unread_messages, marked_unread, prev_batch
|
unread_highlights, unread_notifications, unread_messages, marked_unread, prev_batch
|
||||||
FROM room
|
FROM room
|
||||||
|
@ -42,18 +43,19 @@ const (
|
||||||
name_quality = CASE WHEN $4 IS NOT NULL THEN $5 ELSE room.name_quality END,
|
name_quality = CASE WHEN $4 IS NOT NULL THEN $5 ELSE room.name_quality END,
|
||||||
avatar = COALESCE($6, room.avatar),
|
avatar = COALESCE($6, room.avatar),
|
||||||
explicit_avatar = CASE WHEN $6 IS NOT NULL THEN $7 ELSE room.explicit_avatar END,
|
explicit_avatar = CASE WHEN $6 IS NOT NULL THEN $7 ELSE room.explicit_avatar END,
|
||||||
topic = COALESCE($8, room.topic),
|
dm_user_id = COALESCE($8, room.dm_user_id),
|
||||||
canonical_alias = COALESCE($9, room.canonical_alias),
|
topic = COALESCE($9, room.topic),
|
||||||
lazy_load_summary = COALESCE($10, room.lazy_load_summary),
|
canonical_alias = COALESCE($10, room.canonical_alias),
|
||||||
encryption_event = COALESCE($11, room.encryption_event),
|
lazy_load_summary = COALESCE($11, room.lazy_load_summary),
|
||||||
has_member_list = room.has_member_list OR $12,
|
encryption_event = COALESCE($12, room.encryption_event),
|
||||||
preview_event_rowid = COALESCE($13, room.preview_event_rowid),
|
has_member_list = room.has_member_list OR $13,
|
||||||
sorting_timestamp = COALESCE($14, room.sorting_timestamp),
|
preview_event_rowid = COALESCE($14, room.preview_event_rowid),
|
||||||
unread_highlights = COALESCE($15, room.unread_highlights),
|
sorting_timestamp = COALESCE($15, room.sorting_timestamp),
|
||||||
unread_notifications = COALESCE($16, room.unread_notifications),
|
unread_highlights = COALESCE($16, room.unread_highlights),
|
||||||
unread_messages = COALESCE($17, room.unread_messages),
|
unread_notifications = COALESCE($17, room.unread_notifications),
|
||||||
marked_unread = COALESCE($18, room.marked_unread),
|
unread_messages = COALESCE($18, room.unread_messages),
|
||||||
prev_batch = COALESCE($19, room.prev_batch)
|
marked_unread = COALESCE($19, room.marked_unread),
|
||||||
|
prev_batch = COALESCE($20, room.prev_batch)
|
||||||
WHERE room_id = $1
|
WHERE room_id = $1
|
||||||
`
|
`
|
||||||
setRoomPrevBatchQuery = `
|
setRoomPrevBatchQuery = `
|
||||||
|
@ -153,6 +155,7 @@ type Room struct {
|
||||||
NameQuality NameQuality `json:"name_quality"`
|
NameQuality NameQuality `json:"name_quality"`
|
||||||
Avatar *id.ContentURI `json:"avatar,omitempty"`
|
Avatar *id.ContentURI `json:"avatar,omitempty"`
|
||||||
ExplicitAvatar bool `json:"explicit_avatar"`
|
ExplicitAvatar bool `json:"explicit_avatar"`
|
||||||
|
DMUserID *id.UserID `json:"dm_user_id,omitempty"`
|
||||||
Topic *string `json:"topic,omitempty"`
|
Topic *string `json:"topic,omitempty"`
|
||||||
CanonicalAlias *id.RoomAlias `json:"canonical_alias,omitempty"`
|
CanonicalAlias *id.RoomAlias `json:"canonical_alias,omitempty"`
|
||||||
|
|
||||||
|
@ -188,6 +191,10 @@ func (r *Room) CheckChangesAndCopyInto(other *Room) (hasChanges bool) {
|
||||||
other.ExplicitAvatar = r.ExplicitAvatar
|
other.ExplicitAvatar = r.ExplicitAvatar
|
||||||
hasChanges = true
|
hasChanges = true
|
||||||
}
|
}
|
||||||
|
if r.DMUserID != nil {
|
||||||
|
other.DMUserID = r.DMUserID
|
||||||
|
hasChanges = true
|
||||||
|
}
|
||||||
if r.Topic != nil {
|
if r.Topic != nil {
|
||||||
other.Topic = r.Topic
|
other.Topic = r.Topic
|
||||||
hasChanges = true
|
hasChanges = true
|
||||||
|
@ -250,6 +257,7 @@ func (r *Room) Scan(row dbutil.Scannable) (*Room, error) {
|
||||||
&r.NameQuality,
|
&r.NameQuality,
|
||||||
&r.Avatar,
|
&r.Avatar,
|
||||||
&r.ExplicitAvatar,
|
&r.ExplicitAvatar,
|
||||||
|
&r.DMUserID,
|
||||||
&r.Topic,
|
&r.Topic,
|
||||||
&r.CanonicalAlias,
|
&r.CanonicalAlias,
|
||||||
dbutil.JSON{Data: &r.LazyLoadSummary},
|
dbutil.JSON{Data: &r.LazyLoadSummary},
|
||||||
|
@ -281,6 +289,7 @@ func (r *Room) sqlVariables() []any {
|
||||||
r.NameQuality,
|
r.NameQuality,
|
||||||
r.Avatar,
|
r.Avatar,
|
||||||
r.ExplicitAvatar,
|
r.ExplicitAvatar,
|
||||||
|
r.DMUserID,
|
||||||
r.Topic,
|
r.Topic,
|
||||||
r.CanonicalAlias,
|
r.CanonicalAlias,
|
||||||
dbutil.JSONPtr(r.LazyLoadSummary),
|
dbutil.JSONPtr(r.LazyLoadSummary),
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
-- v0 -> v10 (compatible with v10+): Latest revision
|
-- v0 -> v11 (compatible with v10+): Latest revision
|
||||||
CREATE TABLE account (
|
CREATE TABLE account (
|
||||||
user_id TEXT NOT NULL PRIMARY KEY,
|
user_id TEXT NOT NULL PRIMARY KEY,
|
||||||
device_id TEXT NOT NULL,
|
device_id TEXT NOT NULL,
|
||||||
|
@ -18,6 +18,7 @@ CREATE TABLE room (
|
||||||
name_quality INTEGER NOT NULL DEFAULT 0,
|
name_quality INTEGER NOT NULL DEFAULT 0,
|
||||||
avatar TEXT,
|
avatar TEXT,
|
||||||
explicit_avatar INTEGER NOT NULL DEFAULT 0,
|
explicit_avatar INTEGER NOT NULL DEFAULT 0,
|
||||||
|
dm_user_id TEXT,
|
||||||
topic TEXT,
|
topic TEXT,
|
||||||
canonical_alias TEXT,
|
canonical_alias TEXT,
|
||||||
lazy_load_summary TEXT,
|
lazy_load_summary TEXT,
|
||||||
|
|
19
pkg/hicli/database/upgrades/11-dm-user-id.sql
Normal file
19
pkg/hicli/database/upgrades/11-dm-user-id.sql
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
-- v11 (compatible with v10+): Store direct chat user ID in database
|
||||||
|
ALTER TABLE room ADD COLUMN dm_user_id TEXT;
|
||||||
|
WITH dm_user_ids AS (
|
||||||
|
SELECT room_id, value
|
||||||
|
FROM room
|
||||||
|
INNER JOIN json_each(lazy_load_summary, '$."m.heroes"')
|
||||||
|
WHERE value NOT IN (SELECT value FROM json_each((
|
||||||
|
SELECT event.content
|
||||||
|
FROM current_state cs
|
||||||
|
INNER JOIN event ON cs.event_rowid = event.rowid
|
||||||
|
WHERE cs.room_id=room.room_id AND cs.event_type='io.element.functional_members' AND cs.state_key=''
|
||||||
|
), '$.service_members'))
|
||||||
|
GROUP BY room_id
|
||||||
|
HAVING COUNT(*) = 1
|
||||||
|
)
|
||||||
|
UPDATE room
|
||||||
|
SET dm_user_id=value
|
||||||
|
FROM dm_user_ids du
|
||||||
|
WHERE room.room_id=du.room_id;
|
|
@ -157,6 +157,8 @@ func (h *HiClient) handleJSONCommand(ctx context.Context, req *JSONCommand) (any
|
||||||
return unmarshalAndCall(req.Data, func(params *resolveAliasParams) (*mautrix.RespAliasResolve, error) {
|
return unmarshalAndCall(req.Data, func(params *resolveAliasParams) (*mautrix.RespAliasResolve, error) {
|
||||||
return h.Client.ResolveAlias(ctx, params.Alias)
|
return h.Client.ResolveAlias(ctx, params.Alias)
|
||||||
})
|
})
|
||||||
|
case "request_openid_token":
|
||||||
|
return h.Client.RequestOpenIDToken(ctx)
|
||||||
case "logout":
|
case "logout":
|
||||||
if h.LogoutFunc == nil {
|
if h.LogoutFunc == nil {
|
||||||
return nil, errors.New("logout not supported")
|
return nil, errors.New("logout not supported")
|
||||||
|
|
|
@ -894,10 +894,11 @@ func (h *HiClient) processStateAndTimeline(
|
||||||
}
|
}
|
||||||
// Calculate name from participants if participants changed and current name was generated from participants, or if the room name was unset
|
// Calculate name from participants if participants changed and current name was generated from participants, or if the room name was unset
|
||||||
if (heroesChanged && updatedRoom.NameQuality <= database.NameQualityParticipants) || updatedRoom.NameQuality == database.NameQualityNil {
|
if (heroesChanged && updatedRoom.NameQuality <= database.NameQualityParticipants) || updatedRoom.NameQuality == database.NameQualityNil {
|
||||||
name, dmAvatarURL, err := h.calculateRoomParticipantName(ctx, room.ID, summary)
|
name, dmAvatarURL, dmUserID, err := h.calculateRoomParticipantName(ctx, room.ID, summary)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to calculate room name: %w", err)
|
return fmt.Errorf("failed to calculate room name: %w", err)
|
||||||
}
|
}
|
||||||
|
updatedRoom.DMUserID = &dmUserID
|
||||||
updatedRoom.Name = &name
|
updatedRoom.Name = &name
|
||||||
updatedRoom.NameQuality = database.NameQualityParticipants
|
updatedRoom.NameQuality = database.NameQualityParticipants
|
||||||
if !dmAvatarURL.IsEmpty() && !room.ExplicitAvatar {
|
if !dmAvatarURL.IsEmpty() && !room.ExplicitAvatar {
|
||||||
|
@ -966,15 +967,15 @@ func joinMemberNames(names []string, totalCount int) string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HiClient) calculateRoomParticipantName(ctx context.Context, roomID id.RoomID, summary *mautrix.LazyLoadSummary) (string, id.ContentURI, error) {
|
func (h *HiClient) calculateRoomParticipantName(ctx context.Context, roomID id.RoomID, summary *mautrix.LazyLoadSummary) (string, id.ContentURI, id.UserID, error) {
|
||||||
var primaryAvatarURL id.ContentURI
|
var primaryAvatarURL id.ContentURI
|
||||||
if summary == nil || len(summary.Heroes) == 0 {
|
if summary == nil || len(summary.Heroes) == 0 {
|
||||||
return "Empty room", primaryAvatarURL, nil
|
return "Empty room", primaryAvatarURL, "", nil
|
||||||
}
|
}
|
||||||
var functionalMembers []id.UserID
|
var functionalMembers []id.UserID
|
||||||
functionalMembersEvt, err := h.DB.CurrentState.Get(ctx, roomID, event.StateElementFunctionalMembers, "")
|
functionalMembersEvt, err := h.DB.CurrentState.Get(ctx, roomID, event.StateElementFunctionalMembers, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", primaryAvatarURL, fmt.Errorf("failed to get %s event: %w", event.StateElementFunctionalMembers.Type, err)
|
return "", primaryAvatarURL, "", fmt.Errorf("failed to get %s event: %w", event.StateElementFunctionalMembers.Type, err)
|
||||||
} else if functionalMembersEvt != nil {
|
} else if functionalMembersEvt != nil {
|
||||||
mautrixEvt := functionalMembersEvt.AsRawMautrix()
|
mautrixEvt := functionalMembersEvt.AsRawMautrix()
|
||||||
_ = mautrixEvt.Content.ParseRaw(mautrixEvt.Type)
|
_ = mautrixEvt.Content.ParseRaw(mautrixEvt.Type)
|
||||||
|
@ -990,16 +991,21 @@ func (h *HiClient) calculateRoomParticipantName(ctx context.Context, roomID id.R
|
||||||
} else if summary.InvitedMemberCount != nil {
|
} else if summary.InvitedMemberCount != nil {
|
||||||
memberCount = *summary.InvitedMemberCount
|
memberCount = *summary.InvitedMemberCount
|
||||||
}
|
}
|
||||||
|
var dmUserID id.UserID
|
||||||
for _, hero := range summary.Heroes {
|
for _, hero := range summary.Heroes {
|
||||||
if slices.Contains(functionalMembers, hero) {
|
if slices.Contains(functionalMembers, hero) {
|
||||||
|
// TODO save member count so push rule evaluation would use the subtracted one?
|
||||||
memberCount--
|
memberCount--
|
||||||
continue
|
continue
|
||||||
} else if len(members) >= 5 {
|
} else if len(members) >= 5 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
if dmUserID == "" {
|
||||||
|
dmUserID = hero
|
||||||
|
}
|
||||||
heroEvt, err := h.DB.CurrentState.Get(ctx, roomID, event.StateMember, hero.String())
|
heroEvt, err := h.DB.CurrentState.Get(ctx, roomID, event.StateMember, hero.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", primaryAvatarURL, fmt.Errorf("failed to get %s's member event: %w", hero, err)
|
return "", primaryAvatarURL, "", fmt.Errorf("failed to get %s's member event: %w", hero, err)
|
||||||
} else if heroEvt == nil {
|
} else if heroEvt == nil {
|
||||||
leftMembers = append(leftMembers, hero.String())
|
leftMembers = append(leftMembers, hero.String())
|
||||||
continue
|
continue
|
||||||
|
@ -1015,19 +1021,28 @@ func (h *HiClient) calculateRoomParticipantName(ctx context.Context, roomID id.R
|
||||||
}
|
}
|
||||||
if membership == "join" || membership == "invite" {
|
if membership == "join" || membership == "invite" {
|
||||||
members = append(members, name)
|
members = append(members, name)
|
||||||
|
dmUserID = hero
|
||||||
} else {
|
} else {
|
||||||
leftMembers = append(leftMembers, name)
|
leftMembers = append(leftMembers, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(members)+len(leftMembers) > 1 || !primaryAvatarURL.IsValid() {
|
if !primaryAvatarURL.IsValid() {
|
||||||
primaryAvatarURL = id.ContentURI{}
|
primaryAvatarURL = id.ContentURI{}
|
||||||
}
|
}
|
||||||
if len(members) > 0 {
|
if len(members) > 0 {
|
||||||
return joinMemberNames(members, memberCount), primaryAvatarURL, nil
|
if len(members) > 1 {
|
||||||
|
primaryAvatarURL = id.ContentURI{}
|
||||||
|
dmUserID = ""
|
||||||
|
}
|
||||||
|
return joinMemberNames(members, memberCount), primaryAvatarURL, dmUserID, nil
|
||||||
} else if len(leftMembers) > 0 {
|
} else if len(leftMembers) > 0 {
|
||||||
return fmt.Sprintf("Empty room (was %s)", joinMemberNames(leftMembers, memberCount)), primaryAvatarURL, nil
|
if len(leftMembers) > 1 {
|
||||||
|
primaryAvatarURL = id.ContentURI{}
|
||||||
|
dmUserID = ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Empty room (was %s)", joinMemberNames(leftMembers, memberCount)), primaryAvatarURL, "", nil
|
||||||
} else {
|
} else {
|
||||||
return "Empty room", primaryAvatarURL, nil
|
return "Empty room", primaryAvatarURL, "", nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -74,7 +74,7 @@ export default tseslint.config(
|
||||||
"quotes": ["error", "double", {allowTemplateLiterals: true}],
|
"quotes": ["error", "double", {allowTemplateLiterals: true}],
|
||||||
"semi": ["error", "never"],
|
"semi": ["error", "never"],
|
||||||
"comma-dangle": ["error", "always-multiline"],
|
"comma-dangle": ["error", "always-multiline"],
|
||||||
"max-len": ["warn", 120],
|
"max-len": ["error", 120],
|
||||||
"space-before-function-paren": ["error", {
|
"space-before-function-paren": ["error", {
|
||||||
"anonymous": "never",
|
"anonymous": "never",
|
||||||
"named": "never",
|
"named": "never",
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import { parseMXC } from "@/util/validation.ts"
|
import { parseMXC } from "@/util/validation.ts"
|
||||||
import { ContentURI, LazyLoadSummary, RoomID, UserID, UserProfile } from "./types"
|
import { ContentURI, RoomID, UserID, UserProfile } from "./types"
|
||||||
|
|
||||||
export const getMediaURL = (mxc?: string, encrypted: boolean = false): string | undefined => {
|
export const getMediaURL = (mxc?: string, encrypted: boolean = false): string | undefined => {
|
||||||
const [server, mediaID] = parseMXC(mxc)
|
const [server, mediaID] = parseMXC(mxc)
|
||||||
|
@ -93,20 +93,12 @@ interface RoomForAvatarURL {
|
||||||
room_id: RoomID
|
room_id: RoomID
|
||||||
name?: string
|
name?: string
|
||||||
dm_user_id?: UserID
|
dm_user_id?: UserID
|
||||||
lazy_load_summary?: LazyLoadSummary
|
|
||||||
avatar?: ContentURI
|
avatar?: ContentURI
|
||||||
avatar_url?: ContentURI
|
avatar_url?: ContentURI
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getRoomAvatarURL = (room: RoomForAvatarURL, avatarOverride?: ContentURI): string | undefined => {
|
export const getRoomAvatarURL = (room: RoomForAvatarURL, avatarOverride?: ContentURI): string | undefined => {
|
||||||
let dmUserID: UserID | undefined
|
return getAvatarURL(room.dm_user_id ?? room.room_id, {
|
||||||
if ("dm_user_id" in room) {
|
|
||||||
dmUserID = room.dm_user_id
|
|
||||||
} else if ("lazy_load_summary" in room) {
|
|
||||||
dmUserID = room.lazy_load_summary?.["m.heroes"]?.length === 1
|
|
||||||
? room.lazy_load_summary["m.heroes"][0] : undefined
|
|
||||||
}
|
|
||||||
return getAvatarURL(dmUserID ?? room.room_id, {
|
|
||||||
displayname: room.name,
|
displayname: room.name,
|
||||||
avatar_url: avatarOverride ?? room.avatar ?? room.avatar_url,
|
avatar_url: avatarOverride ?? room.avatar ?? room.avatar_url,
|
||||||
})
|
})
|
||||||
|
|
|
@ -32,6 +32,7 @@ import type {
|
||||||
ReceiptType,
|
ReceiptType,
|
||||||
RelatesTo,
|
RelatesTo,
|
||||||
ResolveAliasResponse,
|
ResolveAliasResponse,
|
||||||
|
RespOpenIDToken,
|
||||||
RespRoomJoin,
|
RespRoomJoin,
|
||||||
RoomAlias,
|
RoomAlias,
|
||||||
RoomID,
|
RoomID,
|
||||||
|
@ -261,4 +262,8 @@ export default abstract class RPCClient {
|
||||||
verify(recovery_key: string): Promise<boolean> {
|
verify(recovery_key: string): Promise<boolean> {
|
||||||
return this.request("verify", { recovery_key })
|
return this.request("verify", { recovery_key })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requestOpenIDToken(): Promise<RespOpenIDToken> {
|
||||||
|
return this.request("request_openid_token", {})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
export * from "./main.ts"
|
export * from "./main.ts"
|
||||||
export * from "./room.ts"
|
export * from "./room.ts"
|
||||||
export * from "./hooks.ts"
|
export * from "./hooks.ts"
|
||||||
|
export * from "./space.ts"
|
||||||
|
|
|
@ -39,7 +39,7 @@ import {
|
||||||
} from "../types"
|
} from "../types"
|
||||||
import { InvitedRoomStore } from "./invitedroom.ts"
|
import { InvitedRoomStore } from "./invitedroom.ts"
|
||||||
import { RoomStateStore } from "./room.ts"
|
import { RoomStateStore } from "./room.ts"
|
||||||
import { DirectChatSpace, RoomListFilter, SpaceEdgeStore, SpaceOrphansSpace, UnreadsSpace } from "./space.ts"
|
import { DirectChatSpace, RoomListFilter, Space, SpaceEdgeStore, SpaceOrphansSpace, UnreadsSpace } from "./space.ts"
|
||||||
|
|
||||||
export interface RoomListEntry {
|
export interface RoomListEntry {
|
||||||
room_id: RoomID
|
room_id: RoomID
|
||||||
|
@ -73,11 +73,12 @@ export class StateStore {
|
||||||
readonly rooms: Map<RoomID, RoomStateStore> = new Map()
|
readonly rooms: Map<RoomID, RoomStateStore> = new Map()
|
||||||
readonly inviteRooms: Map<RoomID, InvitedRoomStore> = new Map()
|
readonly inviteRooms: Map<RoomID, InvitedRoomStore> = new Map()
|
||||||
readonly roomList = new NonNullCachedEventDispatcher<RoomListEntry[]>([])
|
readonly roomList = new NonNullCachedEventDispatcher<RoomListEntry[]>([])
|
||||||
|
readonly roomListEntries = new Map<RoomID, RoomListEntry>()
|
||||||
readonly topLevelSpaces = new NonNullCachedEventDispatcher<RoomID[]>([])
|
readonly topLevelSpaces = new NonNullCachedEventDispatcher<RoomID[]>([])
|
||||||
readonly spaceEdges: Map<RoomID, SpaceEdgeStore> = new Map()
|
readonly spaceEdges: Map<RoomID, SpaceEdgeStore> = new Map()
|
||||||
readonly spaceOrphans = new SpaceOrphansSpace(this)
|
readonly spaceOrphans = new SpaceOrphansSpace(this)
|
||||||
readonly directChatsSpace = new DirectChatSpace()
|
readonly directChatsSpace = new DirectChatSpace()
|
||||||
readonly unreadsSpace = new UnreadsSpace()
|
readonly unreadsSpace = new UnreadsSpace(this)
|
||||||
readonly pseudoSpaces = [
|
readonly pseudoSpaces = [
|
||||||
this.spaceOrphans,
|
this.spaceOrphans,
|
||||||
this.directChatsSpace,
|
this.directChatsSpace,
|
||||||
|
@ -110,6 +111,39 @@ export class StateStore {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSpaceByID(spaceID: string | undefined): RoomListFilter | null {
|
||||||
|
if (!spaceID) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const realSpace = this.spaceEdges.get(spaceID)
|
||||||
|
if (realSpace) {
|
||||||
|
return realSpace
|
||||||
|
}
|
||||||
|
for (const pseudoSpace of this.pseudoSpaces) {
|
||||||
|
if (pseudoSpace.id === spaceID) {
|
||||||
|
return pseudoSpace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.warn("Failed to find space", spaceID)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
findMatchingSpace(room: RoomListEntry): Space | null {
|
||||||
|
if (this.spaceOrphans.include(room)) {
|
||||||
|
return this.spaceOrphans
|
||||||
|
}
|
||||||
|
for (const spaceID of this.topLevelSpaces.current) {
|
||||||
|
const space = this.spaceEdges.get(spaceID)
|
||||||
|
if (space?.include(room)) {
|
||||||
|
return space
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.directChatsSpace.include(room)) {
|
||||||
|
return this.directChatsSpace
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
get roomListFilterFunc(): ((entry: RoomListEntry) => boolean) | null {
|
get roomListFilterFunc(): ((entry: RoomListEntry) => boolean) | null {
|
||||||
if (!this.currentRoomListFilter && !this.currentRoomListQuery) {
|
if (!this.currentRoomListFilter && !this.currentRoomListQuery) {
|
||||||
return null
|
return null
|
||||||
|
@ -169,8 +203,7 @@ export class StateStore {
|
||||||
const name = entry.meta.name ?? "Unnamed room"
|
const name = entry.meta.name ?? "Unnamed room"
|
||||||
return {
|
return {
|
||||||
room_id: entry.meta.room_id,
|
room_id: entry.meta.room_id,
|
||||||
dm_user_id: entry.meta.lazy_load_summary?.["m.heroes"]?.length === 1
|
dm_user_id: entry.meta.dm_user_id,
|
||||||
? entry.meta.lazy_load_summary["m.heroes"][0] : undefined,
|
|
||||||
sorting_timestamp: entry.meta.sorting_timestamp,
|
sorting_timestamp: entry.meta.sorting_timestamp,
|
||||||
preview_event,
|
preview_event,
|
||||||
preview_sender,
|
preview_sender,
|
||||||
|
@ -212,11 +245,11 @@ export class StateStore {
|
||||||
const changedRoomListEntries = new Map<RoomID, RoomListEntry | null>()
|
const changedRoomListEntries = new Map<RoomID, RoomListEntry | null>()
|
||||||
for (const data of sync.invited_rooms ?? []) {
|
for (const data of sync.invited_rooms ?? []) {
|
||||||
const room = new InvitedRoomStore(data, this)
|
const room = new InvitedRoomStore(data, this)
|
||||||
const oldEntry = this.inviteRooms.get(room.room_id)
|
|
||||||
this.inviteRooms.set(room.room_id, room)
|
this.inviteRooms.set(room.room_id, room)
|
||||||
if (!resyncRoomList) {
|
if (!resyncRoomList) {
|
||||||
changedRoomListEntries.set(room.room_id, room)
|
changedRoomListEntries.set(room.room_id, room)
|
||||||
this.#applyUnreadModification(room, oldEntry)
|
this.#applyUnreadModification(room, this.roomListEntries.get(room.room_id))
|
||||||
|
this.roomListEntries.set(room.room_id, room)
|
||||||
}
|
}
|
||||||
if (this.activeRoomID === room.room_id) {
|
if (this.activeRoomID === room.room_id) {
|
||||||
this.switchRoom?.(room.room_id)
|
this.switchRoom?.(room.room_id)
|
||||||
|
@ -239,8 +272,12 @@ export class StateStore {
|
||||||
if (roomListEntryChanged) {
|
if (roomListEntryChanged) {
|
||||||
const entry = this.#makeRoomListEntry(data, room)
|
const entry = this.#makeRoomListEntry(data, room)
|
||||||
changedRoomListEntries.set(roomID, entry)
|
changedRoomListEntries.set(roomID, entry)
|
||||||
this.#applyUnreadModification(entry, room.roomListEntry)
|
this.#applyUnreadModification(entry, this.roomListEntries.get(roomID))
|
||||||
room.roomListEntry = entry
|
if (entry) {
|
||||||
|
this.roomListEntries.set(roomID, entry)
|
||||||
|
} else {
|
||||||
|
this.roomListEntries.delete(roomID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!resyncRoomList) {
|
if (!resyncRoomList) {
|
||||||
// When we join a valid replacement room, hide the tombstoned room.
|
// When we join a valid replacement room, hide the tombstoned room.
|
||||||
|
@ -289,7 +326,7 @@ export class StateStore {
|
||||||
updatedRoomList.sort((r1, r2) => r1.sorting_timestamp - r2.sorting_timestamp)
|
updatedRoomList.sort((r1, r2) => r1.sorting_timestamp - r2.sorting_timestamp)
|
||||||
for (const entry of updatedRoomList) {
|
for (const entry of updatedRoomList) {
|
||||||
this.#applyUnreadModification(entry, undefined)
|
this.#applyUnreadModification(entry, undefined)
|
||||||
this.rooms.get(entry.room_id)!.roomListEntry = entry
|
this.roomListEntries.set(entry.room_id, entry)
|
||||||
}
|
}
|
||||||
} else if (changedRoomListEntries.size > 0) {
|
} else if (changedRoomListEntries.size > 0) {
|
||||||
updatedRoomList = this.roomList.current.filter(entry => !changedRoomListEntries.has(entry.room_id))
|
updatedRoomList = this.roomList.current.filter(entry => !changedRoomListEntries.has(entry.room_id))
|
||||||
|
@ -515,6 +552,7 @@ export class StateStore {
|
||||||
this.rooms.clear()
|
this.rooms.clear()
|
||||||
this.inviteRooms.clear()
|
this.inviteRooms.clear()
|
||||||
this.spaceEdges.clear()
|
this.spaceEdges.clear()
|
||||||
|
this.pseudoSpaces.forEach(space => space.clearUnreads())
|
||||||
this.roomList.emit([])
|
this.roomList.emit([])
|
||||||
this.topLevelSpaces.emit([])
|
this.topLevelSpaces.emit([])
|
||||||
this.accountData.clear()
|
this.accountData.clear()
|
||||||
|
|
|
@ -42,7 +42,7 @@ import {
|
||||||
UserID,
|
UserID,
|
||||||
roomStateGUIDToString,
|
roomStateGUIDToString,
|
||||||
} from "../types"
|
} from "../types"
|
||||||
import type { RoomListEntry, StateStore } from "./main.ts"
|
import type { StateStore } from "./main.ts"
|
||||||
|
|
||||||
function arraysAreEqual<T>(arr1?: T[], arr2?: T[]): boolean {
|
function arraysAreEqual<T>(arr1?: T[], arr2?: T[]): boolean {
|
||||||
if (!arr1 || !arr2) {
|
if (!arr1 || !arr2) {
|
||||||
|
@ -70,6 +70,7 @@ function visibleMetaIsEqual(meta1: DBRoom, meta2: DBRoom): boolean {
|
||||||
meta1.avatar === meta2.avatar &&
|
meta1.avatar === meta2.avatar &&
|
||||||
meta1.topic === meta2.topic &&
|
meta1.topic === meta2.topic &&
|
||||||
meta1.canonical_alias === meta2.canonical_alias &&
|
meta1.canonical_alias === meta2.canonical_alias &&
|
||||||
|
meta1.dm_user_id === meta2.dm_user_id &&
|
||||||
llSummaryIsEqual(meta1.lazy_load_summary, meta2.lazy_load_summary) &&
|
llSummaryIsEqual(meta1.lazy_load_summary, meta2.lazy_load_summary) &&
|
||||||
meta1.encryption_event?.algorithm === meta2.encryption_event?.algorithm &&
|
meta1.encryption_event?.algorithm === meta2.encryption_event?.algorithm &&
|
||||||
meta1.has_member_list === meta2.has_member_list
|
meta1.has_member_list === meta2.has_member_list
|
||||||
|
@ -126,7 +127,6 @@ export class RoomStateStore {
|
||||||
readUpToRow = -1
|
readUpToRow = -1
|
||||||
hasMoreHistory = true
|
hasMoreHistory = true
|
||||||
hidden = false
|
hidden = false
|
||||||
roomListEntry: RoomListEntry | undefined | null
|
|
||||||
|
|
||||||
constructor(meta: DBRoom, private parent: StateStore) {
|
constructor(meta: DBRoom, private parent: StateStore) {
|
||||||
this.roomID = meta.room_id
|
this.roomID = meta.room_id
|
||||||
|
|
|
@ -40,6 +40,10 @@ export abstract class Space implements RoomListFilter {
|
||||||
abstract id: string
|
abstract id: string
|
||||||
abstract include(room: RoomListEntry): boolean
|
abstract include(room: RoomListEntry): boolean
|
||||||
|
|
||||||
|
clearUnreads() {
|
||||||
|
this.counts.emit(emptyUnreadCounts)
|
||||||
|
}
|
||||||
|
|
||||||
applyUnreads(newCounts?: SpaceUnreadCounts | null, oldCounts?: SpaceUnreadCounts | null) {
|
applyUnreads(newCounts?: SpaceUnreadCounts | null, oldCounts?: SpaceUnreadCounts | null) {
|
||||||
const mergedCounts: SpaceUnreadCounts = {
|
const mergedCounts: SpaceUnreadCounts = {
|
||||||
unread_messages: this.counts.current.unread_messages
|
unread_messages: this.counts.current.unread_messages
|
||||||
|
@ -79,8 +83,13 @@ export class DirectChatSpace extends Space {
|
||||||
export class UnreadsSpace extends Space {
|
export class UnreadsSpace extends Space {
|
||||||
id = "fi.mau.gomuks.unreads"
|
id = "fi.mau.gomuks.unreads"
|
||||||
|
|
||||||
|
constructor(private parent: StateStore) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
include(room: RoomListEntry): boolean {
|
include(room: RoomListEntry): boolean {
|
||||||
return Boolean(room.unread_messages
|
return Boolean(room.room_id === this.parent.activeRoomID
|
||||||
|
|| room.unread_messages
|
||||||
|| room.unread_notifications
|
|| room.unread_notifications
|
||||||
|| room.unread_highlights
|
|| room.unread_highlights
|
||||||
|| room.marked_unread)
|
|| room.marked_unread)
|
||||||
|
|
|
@ -54,6 +54,7 @@ export interface DBRoom {
|
||||||
name_quality: RoomNameQuality
|
name_quality: RoomNameQuality
|
||||||
avatar?: ContentURI
|
avatar?: ContentURI
|
||||||
explicit_avatar: boolean
|
explicit_avatar: boolean
|
||||||
|
dm_user_id?: UserID
|
||||||
topic?: string
|
topic?: string
|
||||||
canonical_alias?: RoomAlias
|
canonical_alias?: RoomAlias
|
||||||
lazy_load_summary?: LazyLoadSummary
|
lazy_load_summary?: LazyLoadSummary
|
||||||
|
|
|
@ -288,6 +288,13 @@ export interface RespRoomJoin {
|
||||||
room_id: RoomID
|
room_id: RoomID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RespOpenIDToken {
|
||||||
|
access_token: string
|
||||||
|
expires_in: number
|
||||||
|
matrix_server_name: string
|
||||||
|
token_type: "Bearer"
|
||||||
|
}
|
||||||
|
|
||||||
export interface PronounSet {
|
export interface PronounSet {
|
||||||
subject?: string
|
subject?: string
|
||||||
object?: string
|
object?: string
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
import { JSX, use, useEffect, useMemo, useReducer, useRef, useState } from "react"
|
import { JSX, use, useEffect, useMemo, useReducer, useRef, useState } from "react"
|
||||||
import { SyncLoader } from "react-spinners"
|
import { SyncLoader } from "react-spinners"
|
||||||
import Client from "@/api/client.ts"
|
import Client from "@/api/client.ts"
|
||||||
import { RoomStateStore } from "@/api/statestore"
|
import { RoomListFilter, RoomStateStore } from "@/api/statestore"
|
||||||
import type { RoomID } from "@/api/types"
|
import type { RoomID } from "@/api/types"
|
||||||
import { useEventAsState } from "@/util/eventdispatcher.ts"
|
import { useEventAsState } from "@/util/eventdispatcher.ts"
|
||||||
import { ensureString, ensureStringArray, parseMatrixURI } from "@/util/validation.ts"
|
import { ensureString, ensureStringArray, parseMatrixURI } from "@/util/validation.ts"
|
||||||
|
@ -52,6 +52,7 @@ class ContextFields implements MainScreenContextFields {
|
||||||
constructor(
|
constructor(
|
||||||
private directSetRightPanel: (props: RightPanelProps | null) => void,
|
private directSetRightPanel: (props: RightPanelProps | null) => void,
|
||||||
private directSetActiveRoom: (room: RoomStateStore | RoomPreviewProps | null) => void,
|
private directSetActiveRoom: (room: RoomStateStore | RoomPreviewProps | null) => void,
|
||||||
|
private directSetSpace: (space: RoomListFilter | null) => void,
|
||||||
private client: Client,
|
private client: Client,
|
||||||
) {
|
) {
|
||||||
this.keybindings = new Keybindings(client.store, this)
|
this.keybindings = new Keybindings(client.store, this)
|
||||||
|
@ -95,12 +96,17 @@ class ContextFields implements MainScreenContextFields {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setActiveRoom = (roomID: RoomID | null, previewMeta?: Partial<RoomPreviewProps>, pushState = true) => {
|
setActiveRoom = (
|
||||||
|
roomID: RoomID | null,
|
||||||
|
previewMeta?: Partial<RoomPreviewProps>,
|
||||||
|
toSpace?: RoomListFilter,
|
||||||
|
pushState = true,
|
||||||
|
) => {
|
||||||
console.log("Switching to room", roomID)
|
console.log("Switching to room", roomID)
|
||||||
if (roomID) {
|
if (roomID) {
|
||||||
const room = this.client.store.rooms.get(roomID)
|
const room = this.client.store.rooms.get(roomID)
|
||||||
if (room) {
|
if (room) {
|
||||||
this.#setActiveRoom(room, pushState)
|
this.#setActiveRoom(room, toSpace, pushState)
|
||||||
} else {
|
} else {
|
||||||
this.#setPreviewRoom(roomID, pushState, previewMeta)
|
this.#setPreviewRoom(roomID, pushState, previewMeta)
|
||||||
}
|
}
|
||||||
|
@ -109,6 +115,24 @@ class ContextFields implements MainScreenContextFields {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSpace = (space: RoomListFilter | null, pushState = true) => {
|
||||||
|
if (space === this.client.store.currentRoomListFilter) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log("Switching to space", space?.id)
|
||||||
|
this.directSetSpace(space)
|
||||||
|
this.client.store.currentRoomListFilter = space
|
||||||
|
if (pushState) {
|
||||||
|
if (this.client.store.activeRoomID && space) {
|
||||||
|
const entry = this.client.store.roomListEntries.get(this.client.store.activeRoomID)
|
||||||
|
if (entry && !space.include(entry)) {
|
||||||
|
this.setActiveRoom(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
history.replaceState({ ...(history.state || {}), space_id: space?.id }, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#setPreviewRoom(roomID: RoomID, pushState: boolean, meta?: Partial<RoomPreviewProps>) {
|
#setPreviewRoom(roomID: RoomID, pushState: boolean, meta?: Partial<RoomPreviewProps>) {
|
||||||
const invite = this.client.store.inviteRooms.get(roomID)
|
const invite = this.client.store.inviteRooms.get(roomID)
|
||||||
this.#closeActiveRoom(false)
|
this.#closeActiveRoom(false)
|
||||||
|
@ -120,6 +144,7 @@ class ContextFields implements MainScreenContextFields {
|
||||||
room_id: roomID,
|
room_id: roomID,
|
||||||
source_via: meta?.via,
|
source_via: meta?.via,
|
||||||
source_alias: meta?.alias,
|
source_alias: meta?.alias,
|
||||||
|
space_id: history.state?.space_id,
|
||||||
}, "")
|
}, "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -131,10 +156,21 @@ class ContextFields implements MainScreenContextFields {
|
||||||
return room.preferences.room_window_title.replace("$room", name!)
|
return room.preferences.room_window_title.replace("$room", name!)
|
||||||
}
|
}
|
||||||
|
|
||||||
#setActiveRoom(room: RoomStateStore, pushState: boolean) {
|
#setActiveRoom(room: RoomStateStore, space: RoomListFilter | undefined | null, pushState: boolean) {
|
||||||
window.activeRoom = room
|
window.activeRoom = room
|
||||||
this.directSetActiveRoom(room)
|
this.directSetActiveRoom(room)
|
||||||
this.directSetRightPanel(null)
|
this.directSetRightPanel(null)
|
||||||
|
if (!space && this.client.store.currentRoomListFilter) {
|
||||||
|
const roomListEntry = this.client.store.roomListEntries.get(room.roomID)
|
||||||
|
if (roomListEntry && !this.client.store.currentRoomListFilter.include(roomListEntry)) {
|
||||||
|
space = this.client.store.findMatchingSpace(roomListEntry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (space && space !== this.client.store.currentRoomListFilter) {
|
||||||
|
console.log("Switching to space", space?.id)
|
||||||
|
this.directSetSpace(space)
|
||||||
|
this.client.store.currentRoomListFilter = space
|
||||||
|
}
|
||||||
this.rightPanelStack = []
|
this.rightPanelStack = []
|
||||||
this.client.store.activeRoomID = room.roomID
|
this.client.store.activeRoomID = room.roomID
|
||||||
this.client.store.activeRoomIsPreview = false
|
this.client.store.activeRoomIsPreview = false
|
||||||
|
@ -148,7 +184,7 @@ class ContextFields implements MainScreenContextFields {
|
||||||
.querySelector(`div.room-entry[data-room-id="${CSS.escape(room.roomID)}"]`)
|
.querySelector(`div.room-entry[data-room-id="${CSS.escape(room.roomID)}"]`)
|
||||||
?.scrollIntoView({ block: "nearest" })
|
?.scrollIntoView({ block: "nearest" })
|
||||||
if (pushState) {
|
if (pushState) {
|
||||||
history.pushState({ room_id: room.roomID }, "")
|
history.pushState({ room_id: room.roomID, space_id: space?.id ?? history.state?.space_id }, "")
|
||||||
}
|
}
|
||||||
let roomNameForTitle = room.meta.current.name
|
let roomNameForTitle = room.meta.current.name
|
||||||
if (roomNameForTitle && roomNameForTitle.length > 48) {
|
if (roomNameForTitle && roomNameForTitle.length > 48) {
|
||||||
|
@ -166,7 +202,7 @@ class ContextFields implements MainScreenContextFields {
|
||||||
this.client.store.activeRoomIsPreview = false
|
this.client.store.activeRoomIsPreview = false
|
||||||
this.keybindings.activeRoom = null
|
this.keybindings.activeRoom = null
|
||||||
if (pushState) {
|
if (pushState) {
|
||||||
history.pushState({}, "")
|
history.pushState({ space_id: history.state?.space_id }, "")
|
||||||
}
|
}
|
||||||
document.title = this.#getWindowTitle()
|
document.title = this.#getWindowTitle()
|
||||||
}
|
}
|
||||||
|
@ -197,7 +233,7 @@ class ContextFields implements MainScreenContextFields {
|
||||||
|
|
||||||
const SYNC_ERROR_HIDE_DELAY = 30 * 1000
|
const SYNC_ERROR_HIDE_DELAY = 30 * 1000
|
||||||
|
|
||||||
const handleURLHash = (client: Client) => {
|
const handleURLHash = (client: Client, context: MainScreenContextFields) => {
|
||||||
if (!location.hash.startsWith("#/uri/")) {
|
if (!location.hash.startsWith("#/uri/")) {
|
||||||
if (location.search) {
|
if (location.search) {
|
||||||
const currentETag = (
|
const currentETag = (
|
||||||
|
@ -248,7 +284,7 @@ const handleURLHash = (client: Client) => {
|
||||||
// TODO loading indicator or something for this?
|
// TODO loading indicator or something for this?
|
||||||
client.rpc.resolveAlias(uri.identifier).then(
|
client.rpc.resolveAlias(uri.identifier).then(
|
||||||
res => {
|
res => {
|
||||||
window.mainScreenContext.setActiveRoom(res.room_id, {
|
context.setActiveRoom(res.room_id, {
|
||||||
alias: uri.identifier,
|
alias: uri.identifier,
|
||||||
via: res.servers.slice(0, 3),
|
via: res.servers.slice(0, 3),
|
||||||
})
|
})
|
||||||
|
@ -279,12 +315,13 @@ const activeRoomReducer = (
|
||||||
|
|
||||||
const MainScreen = () => {
|
const MainScreen = () => {
|
||||||
const [[prevActiveRoom, activeRoom], directSetActiveRoom] = useReducer(activeRoomReducer, [null, null])
|
const [[prevActiveRoom, activeRoom], directSetActiveRoom] = useReducer(activeRoomReducer, [null, null])
|
||||||
|
const [space, directSetSpace] = useState<RoomListFilter | null>(null)
|
||||||
const skipNextTransitionRef = useRef(false)
|
const skipNextTransitionRef = useRef(false)
|
||||||
const [rightPanel, directSetRightPanel] = useState<RightPanelProps | null>(null)
|
const [rightPanel, directSetRightPanel] = useState<RightPanelProps | null>(null)
|
||||||
const client = use(ClientContext)!
|
const client = use(ClientContext)!
|
||||||
const syncStatus = useEventAsState(client.syncStatus)
|
const syncStatus = useEventAsState(client.syncStatus)
|
||||||
const context = useMemo(
|
const context = useMemo(
|
||||||
() => new ContextFields(directSetRightPanel, directSetActiveRoom, client),
|
() => new ContextFields(directSetRightPanel, directSetActiveRoom, directSetSpace, client),
|
||||||
[client],
|
[client],
|
||||||
)
|
)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -292,17 +329,21 @@ const MainScreen = () => {
|
||||||
const listener = (evt: PopStateEvent) => {
|
const listener = (evt: PopStateEvent) => {
|
||||||
skipNextTransitionRef.current = evt.hasUAVisualTransition
|
skipNextTransitionRef.current = evt.hasUAVisualTransition
|
||||||
const roomID = evt.state?.room_id ?? null
|
const roomID = evt.state?.room_id ?? null
|
||||||
|
const spaceID = evt.state?.space_id ?? undefined
|
||||||
|
if (spaceID !== client.store.currentRoomListFilter?.id) {
|
||||||
|
context.setSpace(client.store.getSpaceByID(spaceID), false)
|
||||||
|
}
|
||||||
if (roomID !== client.store.activeRoomID) {
|
if (roomID !== client.store.activeRoomID) {
|
||||||
context.setActiveRoom(roomID, {
|
context.setActiveRoom(roomID, {
|
||||||
alias: ensureString(evt.state?.source_alias) || undefined,
|
alias: ensureString(evt.state?.source_alias) || undefined,
|
||||||
via: ensureStringArray(evt.state?.source_via),
|
via: ensureStringArray(evt.state?.source_via),
|
||||||
}, false)
|
}, undefined, false)
|
||||||
}
|
}
|
||||||
context.setRightPanel(evt.state?.right_panel ?? null, false)
|
context.setRightPanel(evt.state?.right_panel ?? null, false)
|
||||||
}
|
}
|
||||||
window.addEventListener("popstate", listener)
|
window.addEventListener("popstate", listener)
|
||||||
const initHandle = () => {
|
const initHandle = () => {
|
||||||
const state = handleURLHash(client)
|
const state = handleURLHash(client, context)
|
||||||
listener({ state } as PopStateEvent)
|
listener({ state } as PopStateEvent)
|
||||||
}
|
}
|
||||||
let cancel = () => {}
|
let cancel = () => {}
|
||||||
|
@ -372,7 +413,7 @@ const MainScreen = () => {
|
||||||
<ModalWrapper>
|
<ModalWrapper>
|
||||||
<StylePreferences client={client} activeRoom={activeRealRoom}/>
|
<StylePreferences client={client} activeRoom={activeRealRoom}/>
|
||||||
<main className={classNames.join(" ")} style={extraStyle}>
|
<main className={classNames.join(" ")} style={extraStyle}>
|
||||||
<RoomList activeRoomID={activeRoom?.roomID ?? null}/>
|
<RoomList activeRoomID={activeRoom?.roomID ?? null} space={space}/>
|
||||||
{resizeHandle1}
|
{resizeHandle1}
|
||||||
{renderedRoom
|
{renderedRoom
|
||||||
? renderedRoom instanceof RoomStateStore
|
? renderedRoom instanceof RoomStateStore
|
||||||
|
|
|
@ -13,13 +13,15 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import { createContext } from "react"
|
import React, { createContext } from "react"
|
||||||
|
import { RoomListFilter } from "@/api/statestore"
|
||||||
import type { RoomID } from "@/api/types"
|
import type { RoomID } from "@/api/types"
|
||||||
import type { RightPanelProps } from "./rightpanel/RightPanel.tsx"
|
import type { RightPanelProps } from "./rightpanel/RightPanel.tsx"
|
||||||
import type { RoomPreviewProps } from "./roomview/RoomPreview.tsx"
|
import type { RoomPreviewProps } from "./roomview/RoomPreview.tsx"
|
||||||
|
|
||||||
export interface MainScreenContextFields {
|
export interface MainScreenContextFields {
|
||||||
setActiveRoom: (roomID: RoomID | null, previewMeta?: Partial<RoomPreviewProps>) => void
|
setActiveRoom: (roomID: RoomID | null, previewMeta?: Partial<RoomPreviewProps>, toSpace?: RoomListFilter) => void
|
||||||
|
setSpace: (space: RoomListFilter | null, pushState?: boolean) => void
|
||||||
clickRoom: (evt: React.MouseEvent) => void
|
clickRoom: (evt: React.MouseEvent) => void
|
||||||
clearActiveRoom: () => void
|
clearActiveRoom: () => void
|
||||||
|
|
||||||
|
@ -32,6 +34,9 @@ const stubContext = {
|
||||||
get setActiveRoom(): never {
|
get setActiveRoom(): never {
|
||||||
throw new Error("MainScreenContext used outside main screen")
|
throw new Error("MainScreenContext used outside main screen")
|
||||||
},
|
},
|
||||||
|
get setSpace(): never {
|
||||||
|
throw new Error("MainScreenContext used outside main screen")
|
||||||
|
},
|
||||||
get clickRoom(): never {
|
get clickRoom(): never {
|
||||||
throw new Error("MainScreenContext used outside main screen")
|
throw new Error("MainScreenContext used outside main screen")
|
||||||
},
|
},
|
||||||
|
|
|
@ -60,7 +60,7 @@ const ModalWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||||
}
|
}
|
||||||
window.addEventListener("popstate", listener)
|
window.addEventListener("popstate", listener)
|
||||||
return () => window.removeEventListener("popstate", listener)
|
return () => window.removeEventListener("popstate", listener)
|
||||||
}, [])
|
}, [onClickWrapper])
|
||||||
let modal: JSX.Element | null = null
|
let modal: JSX.Element | null = null
|
||||||
if (state) {
|
if (state) {
|
||||||
let content = <ModalCloseContext value={onClickWrapper}>{state.content}</ModalCloseContext>
|
let content = <ModalCloseContext value={onClickWrapper}>{state.content}</ModalCloseContext>
|
||||||
|
|
|
@ -40,8 +40,7 @@ const MutualRooms = ({ client, userID }: MutualRoomsProps) => {
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
room_id: roomID,
|
room_id: roomID,
|
||||||
dm_user_id: roomData.meta.current.lazy_load_summary?.["m.heroes"]?.length === 1
|
dm_user_id: roomData.meta.current.dm_user_id,
|
||||||
? roomData.meta.current.lazy_load_summary["m.heroes"][0] : undefined,
|
|
||||||
name: roomData.meta.current.name ?? "Unnamed room",
|
name: roomData.meta.current.name ?? "Unnamed room",
|
||||||
avatar: roomData.meta.current.avatar,
|
avatar: roomData.meta.current.avatar,
|
||||||
search_name: "",
|
search_name: "",
|
||||||
|
|
|
@ -27,30 +27,33 @@ export interface FakeSpaceProps {
|
||||||
space: Space | null
|
space: Space | null
|
||||||
setSpace: (space: RoomListFilter | null) => void
|
setSpace: (space: RoomListFilter | null) => void
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
onClickUnread?: (evt: React.MouseEvent<HTMLDivElement> | null, space: Space | null) => void
|
onClickUnread?: (evt: React.MouseEvent<HTMLDivElement>, space: Space | null) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFakeSpaceIcon = (space: RoomListFilter | null): JSX.Element | null => {
|
const getFakeSpaceMeta = (space: RoomListFilter | null): [string | undefined, JSX.Element | null] => {
|
||||||
switch (space?.id) {
|
switch (space?.id) {
|
||||||
case undefined:
|
case undefined:
|
||||||
return <HomeIcon />
|
return ["Home", <HomeIcon />]
|
||||||
case "fi.mau.gomuks.direct_chats":
|
case "fi.mau.gomuks.direct_chats":
|
||||||
return <PersonIcon />
|
return ["Direct chats", <PersonIcon />]
|
||||||
case "fi.mau.gomuks.unreads":
|
case "fi.mau.gomuks.unreads":
|
||||||
return <NotificationsIcon />
|
return ["Unread chats", <NotificationsIcon />]
|
||||||
case "fi.mau.gomuks.space_orphans":
|
case "fi.mau.gomuks.space_orphans":
|
||||||
return <TagIcon />
|
return ["Rooms outside spaces", <TagIcon />]
|
||||||
default:
|
default:
|
||||||
return null
|
return [undefined, null]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const FakeSpace = ({ space, setSpace, isActive, onClickUnread }: FakeSpaceProps) => {
|
const FakeSpace = ({ space, setSpace, isActive, onClickUnread }: FakeSpaceProps) => {
|
||||||
const unreads = useEventAsState(space?.counts)
|
const unreads = useEventAsState(space?.counts)
|
||||||
const onClickUnreadWrapped = onClickUnread ? () => onClickUnread(null, space) : undefined
|
const onClickUnreadWrapped = onClickUnread
|
||||||
return <div className={`space-entry ${isActive ? "active" : ""}`} onClick={() => setSpace(space)}>
|
? (evt: React.MouseEvent<HTMLDivElement>) => onClickUnread(evt, space)
|
||||||
|
: undefined
|
||||||
|
const [title, icon] = getFakeSpaceMeta(space)
|
||||||
|
return <div className={`space-entry ${isActive ? "active" : ""}`} onClick={() => setSpace(space)} title={title}>
|
||||||
<UnreadCount counts={unreads} space={true} onClick={onClickUnreadWrapped} />
|
<UnreadCount counts={unreads} space={true} onClick={onClickUnreadWrapped} />
|
||||||
{getFakeSpaceIcon(space)}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -119,7 +119,7 @@ div.room-entry {
|
||||||
width: 3rem;
|
width: 3rem;
|
||||||
|
|
||||||
> img.room-avatar {
|
> img.room-avatar {
|
||||||
padding: 4px;
|
margin: .25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,6 +172,7 @@ div.room-entry-unreads {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: var(--unread-count-size);
|
border-radius: var(--unread-count-size);
|
||||||
color: var(--unread-counter-text-color);
|
color: var(--unread-counter-text-color);
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
background-color: var(--unread-counter-message-bg);
|
background-color: var(--unread-counter-message-bg);
|
||||||
height: var(--unread-count-size);
|
height: var(--unread-count-size);
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import React, { use, useCallback, useRef, useState } from "react"
|
import React, { use, useCallback, useRef, useState } from "react"
|
||||||
import { RoomListFilter, Space as SpaceStore, SpaceUnreadCounts } from "@/api/statestore/space.ts"
|
import { RoomListFilter, Space as SpaceStore, SpaceUnreadCounts } from "@/api/statestore"
|
||||||
import type { RoomID } from "@/api/types"
|
import type { RoomID } from "@/api/types"
|
||||||
import { useEventAsState } from "@/util/eventdispatcher.ts"
|
import { useEventAsState } from "@/util/eventdispatcher.ts"
|
||||||
import reverseMap from "@/util/reversemap.ts"
|
import reverseMap from "@/util/reversemap.ts"
|
||||||
|
@ -31,42 +31,38 @@ import "./RoomList.css"
|
||||||
|
|
||||||
interface RoomListProps {
|
interface RoomListProps {
|
||||||
activeRoomID: RoomID | null
|
activeRoomID: RoomID | null
|
||||||
|
space: RoomListFilter | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const RoomList = ({ activeRoomID }: RoomListProps) => {
|
const RoomList = ({ activeRoomID, space }: RoomListProps) => {
|
||||||
const client = use(ClientContext)!
|
const client = use(ClientContext)!
|
||||||
const mainScreen = use(MainScreenContext)
|
const mainScreen = use(MainScreenContext)
|
||||||
const roomList = useEventAsState(client.store.roomList)
|
const roomList = useEventAsState(client.store.roomList)
|
||||||
const spaces = useEventAsState(client.store.topLevelSpaces)
|
const spaces = useEventAsState(client.store.topLevelSpaces)
|
||||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||||
const [query, directSetQuery] = useState("")
|
const [query, directSetQuery] = useState("")
|
||||||
const [space, directSetSpace] = useState<RoomListFilter | null>(null)
|
|
||||||
|
|
||||||
const setQuery = (evt: React.ChangeEvent<HTMLInputElement>) => {
|
const setQuery = (evt: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
client.store.currentRoomListQuery = toSearchableString(evt.target.value)
|
client.store.currentRoomListQuery = toSearchableString(evt.target.value)
|
||||||
directSetQuery(evt.target.value)
|
directSetQuery(evt.target.value)
|
||||||
}
|
}
|
||||||
const setSpace = useCallback((space: RoomListFilter | null) => {
|
|
||||||
directSetSpace(space)
|
|
||||||
client.store.currentRoomListFilter = space
|
|
||||||
}, [client])
|
|
||||||
const onClickSpace = useCallback((evt: React.MouseEvent<HTMLDivElement>) => {
|
const onClickSpace = useCallback((evt: React.MouseEvent<HTMLDivElement>) => {
|
||||||
const store = client.store.getSpaceStore(evt.currentTarget.getAttribute("data-target-space")!)
|
const store = client.store.getSpaceStore(evt.currentTarget.getAttribute("data-target-space")!)
|
||||||
setSpace(store)
|
mainScreen.setSpace(store)
|
||||||
}, [setSpace, client])
|
}, [mainScreen, client])
|
||||||
const onClickSpaceUnread = useCallback((
|
const onClickSpaceUnread = useCallback((
|
||||||
evt: React.MouseEvent<HTMLDivElement> | null, space?: SpaceStore | null,
|
evt: React.MouseEvent<HTMLDivElement>, space?: SpaceStore | null,
|
||||||
) => {
|
) => {
|
||||||
if (evt) {
|
if (!space) {
|
||||||
const targetSpace = evt.currentTarget.closest("div.space-entry")?.getAttribute("data-target-space")
|
const targetSpace = evt.currentTarget.closest("div.space-entry")?.getAttribute("data-target-space")
|
||||||
if (!targetSpace) {
|
if (!targetSpace) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
space = client.store.getSpaceStore(targetSpace)
|
space = client.store.getSpaceStore(targetSpace)
|
||||||
}
|
|
||||||
if (!space) {
|
if (!space) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
const counts = space.counts.current
|
const counts = space.counts.current
|
||||||
let wantedField: keyof SpaceUnreadCounts
|
let wantedField: keyof SpaceUnreadCounts
|
||||||
if (counts.unread_highlights > 0) {
|
if (counts.unread_highlights > 0) {
|
||||||
|
@ -81,7 +77,8 @@ const RoomList = ({ activeRoomID }: RoomListProps) => {
|
||||||
for (let i = client.store.roomList.current.length - 1; i >= 0; i--) {
|
for (let i = client.store.roomList.current.length - 1; i >= 0; i--) {
|
||||||
const entry = client.store.roomList.current[i]
|
const entry = client.store.roomList.current[i]
|
||||||
if (entry[wantedField] > 0 && space.include(entry)) {
|
if (entry[wantedField] > 0 && space.include(entry)) {
|
||||||
mainScreen.setActiveRoom(entry.room_id)
|
mainScreen.setActiveRoom(entry.room_id, undefined, space)
|
||||||
|
evt.stopPropagation()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -124,11 +121,11 @@ const RoomList = ({ activeRoomID }: RoomListProps) => {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-bar">
|
<div className="space-bar">
|
||||||
<FakeSpace space={null} setSpace={setSpace} isActive={space === null} />
|
<FakeSpace space={null} setSpace={mainScreen.setSpace} isActive={space === null} />
|
||||||
{client.store.pseudoSpaces.map(pseudoSpace => <FakeSpace
|
{client.store.pseudoSpaces.map(pseudoSpace => <FakeSpace
|
||||||
key={pseudoSpace.id}
|
key={pseudoSpace.id}
|
||||||
space={pseudoSpace}
|
space={pseudoSpace}
|
||||||
setSpace={setSpace}
|
setSpace={mainScreen.setSpace}
|
||||||
onClickUnread={onClickSpaceUnread}
|
onClickUnread={onClickSpaceUnread}
|
||||||
isActive={space?.id === pseudoSpace.id}
|
isActive={space?.id === pseudoSpace.id}
|
||||||
/>)}
|
/>)}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import { SpaceUnreadCounts } from "@/api/statestore/space.ts"
|
import { SpaceUnreadCounts } from "@/api/statestore"
|
||||||
|
|
||||||
interface UnreadCounts extends SpaceUnreadCounts {
|
interface UnreadCounts extends SpaceUnreadCounts {
|
||||||
marked_unread?: boolean
|
marked_unread?: boolean
|
||||||
|
@ -38,9 +38,9 @@ const UnreadCount = ({ counts, space, onClick }: UnreadCountProps) => {
|
||||||
const countIsBig = !space
|
const countIsBig = !space
|
||||||
&& Boolean(counts.unread_notifications || counts.unread_highlights || counts.marked_unread)
|
&& Boolean(counts.unread_notifications || counts.unread_highlights || counts.marked_unread)
|
||||||
let unreadCountDisplay = unreadCount.toString()
|
let unreadCountDisplay = unreadCount.toString()
|
||||||
if (unreadCount > 999 && countIsBig) {
|
if (unreadCount > 999 && (countIsBig || space)) {
|
||||||
unreadCountDisplay = "99+"
|
unreadCountDisplay = "99+"
|
||||||
} else if (unreadCount > 9999 && countIsBig) {
|
} else if (unreadCount > 9999) {
|
||||||
unreadCountDisplay = "999+"
|
unreadCountDisplay = "999+"
|
||||||
}
|
}
|
||||||
const classNames = ["unread-count"]
|
const classNames = ["unread-count"]
|
||||||
|
|
|
@ -237,7 +237,9 @@ const CustomCSSInput = ({ setPref, room }: { setPref: SetPrefFunc, room: RoomSta
|
||||||
</span>}
|
</span>}
|
||||||
</div>
|
</div>
|
||||||
{vscodeOpen ? <div className="vscode-wrapper">
|
{vscodeOpen ? <div className="vscode-wrapper">
|
||||||
<Suspense fallback={<div className="loader"><ScaleLoader width={40} height={80} color="var(--primary-color)"/></div>}>
|
<Suspense fallback={
|
||||||
|
<div className="loader"><ScaleLoader width={40} height={80} color="var(--primary-color)"/></div>
|
||||||
|
}>
|
||||||
<Monaco
|
<Monaco
|
||||||
initData={vscodeInitialContentRef.current}
|
initData={vscodeInitialContentRef.current}
|
||||||
onClose={closeVSCode}
|
onClose={closeVSCode}
|
||||||
|
@ -332,6 +334,16 @@ const SettingsView = ({ room }: SettingsViewProps) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const onClickOpenCSSApp = () => {
|
||||||
|
client.rpc.requestOpenIDToken().then(
|
||||||
|
resp => window.open(
|
||||||
|
`https://css.gomuks.app/login?token=${resp.access_token}&server_name=${resp.matrix_server_name}`,
|
||||||
|
"_blank",
|
||||||
|
"noreferrer noopener",
|
||||||
|
),
|
||||||
|
err => window.alert(`Failed to request OpenID token: ${err}`),
|
||||||
|
)
|
||||||
|
}
|
||||||
usePreferences(client.store, room)
|
usePreferences(client.store, room)
|
||||||
const globalServer = client.store.serverPreferenceCache
|
const globalServer = client.store.serverPreferenceCache
|
||||||
const globalLocal = client.store.localPreferenceCache
|
const globalLocal = client.store.localPreferenceCache
|
||||||
|
@ -381,6 +393,7 @@ const SettingsView = ({ room }: SettingsViewProps) => {
|
||||||
<CustomCSSInput setPref={setPref} room={room} />
|
<CustomCSSInput setPref={setPref} room={room} />
|
||||||
<AppliedSettingsView room={room} />
|
<AppliedSettingsView room={room} />
|
||||||
<div className="misc-buttons">
|
<div className="misc-buttons">
|
||||||
|
<button onClick={onClickOpenCSSApp}>Sign into css.gomuks.app</button>
|
||||||
{window.Notification && <button onClick={client.requestNotificationPermission}>
|
{window.Notification && <button onClick={client.requestNotificationPermission}>
|
||||||
Request notification permission
|
Request notification permission
|
||||||
</button>}
|
</button>}
|
||||||
|
|
|
@ -20,6 +20,7 @@ div.timeline-event > div.read-receipts {
|
||||||
> img {
|
> img {
|
||||||
margin-left: -.35rem;
|
margin-left: -.35rem;
|
||||||
border: 1px solid var(--background-color);
|
border: 1px solid var(--background-color);
|
||||||
|
background-color: var(--background-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue