room-details: Rework navigation and fix listview styles
Fixes: https://gitlab.gnome.org/GNOME/fractal/-/issues/900
This commit is contained in:
parent
f8e9147f7d
commit
15bda14f05
20 changed files with 1531 additions and 450 deletions
|
@ -54,6 +54,8 @@
|
|||
<file compressed="true" preprocess="xml-stripblanks" alias="content-invitee-row.ui">ui/content-invitee-row.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks" alias="content-markdown-popover.ui">ui/content-markdown-popover.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks" alias="content-member-item.ui">ui/content-member-item.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks" alias="content-member-page-list-view.ui">ui/content-member-page-list-view.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks" alias="content-member-page-membership-subpage-row.ui">ui/content-member-page-membership-subpage-row.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks" alias="content-member-page.ui">ui/content-member-page.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks" alias="content-member-row.ui">ui/content-member-row.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks" alias="content-message-audio.ui">ui/content-message-audio.ui</file>
|
||||
|
|
|
@ -62,6 +62,9 @@ button.row {
|
|||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.round-corners {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Components */
|
||||
|
||||
|
@ -545,6 +548,13 @@ message-reactions .reaction-count {
|
|||
font-size: 1.6em;
|
||||
}
|
||||
|
||||
.invite-search-results {
|
||||
padding: 12px 0px;
|
||||
}
|
||||
|
||||
.invite-search-results > row {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Room Details */
|
||||
|
||||
|
@ -552,6 +562,10 @@ message-reactions .reaction-count {
|
|||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.room-details listview {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.room-details-group avatar * {
|
||||
/* Undo non-sensitive style. */
|
||||
filter: none;
|
||||
|
|
|
@ -34,10 +34,10 @@
|
|||
<property name="margin-end">30</property>
|
||||
<property name="margin-start">30</property>
|
||||
<property name="margin-top">6</property>
|
||||
<property name="hexpand">true</property>
|
||||
<property name="hexpand">True</property>
|
||||
<child>
|
||||
<object class="CustomEntry">
|
||||
<!-- FIXME: inserting a Pill makes the Entry grow, therefore we force more height so that it doesn't grow visually
|
||||
<!-- FIXME: inserting a Pill makes the Entry grow, therefore we force more height so that it doesn't grow visually
|
||||
Would be nice to fix it properly. Including the vertical alignment of Pills in the textview
|
||||
-->
|
||||
<property name="height-request">74</property>
|
||||
|
@ -53,7 +53,7 @@
|
|||
<object class="GtkScrolledWindow">
|
||||
<child>
|
||||
<object class="GtkTextView" id="text_view">
|
||||
<property name="hexpand">true</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="justification">left</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="accepts-tab">False</property>
|
||||
|
@ -80,8 +80,6 @@
|
|||
<object class="GtkStack" id="stack">
|
||||
<child>
|
||||
<object class="AdwStatusPage" id="no_search_page">
|
||||
<property name="visible">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="icon-name">system-search-symbolic</property>
|
||||
<property name="description" translatable="yes">Search for users to invite them to this room.</property>
|
||||
|
@ -89,16 +87,12 @@
|
|||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="matching_page">
|
||||
<property name="propagate-natural-height">True</property>
|
||||
<property name="child">
|
||||
<object class="AdwClampScrollable">
|
||||
<property name="child">
|
||||
<object class="GtkListView" id="list_view">
|
||||
<property name="margin-bottom">24</property>
|
||||
<property name="margin-end">12</property>
|
||||
<property name="margin-start">12</property>
|
||||
<property name="margin-top">24</property>
|
||||
<property name="show-separators">True</property>
|
||||
<property name="single-click-activate">True</property>
|
||||
<property name="factory">
|
||||
<object class="GtkBuilderListItemFactory">
|
||||
|
@ -106,7 +100,7 @@
|
|||
</object>
|
||||
</property>
|
||||
<style>
|
||||
<class name="content"/>
|
||||
<class name="invite-search-results"/>
|
||||
</style>
|
||||
</object>
|
||||
</property>
|
||||
|
@ -116,18 +110,12 @@
|
|||
</child>
|
||||
<child>
|
||||
<object class="AdwStatusPage" id="no_matching_page">
|
||||
<property name="visible">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="icon-name">system-search-symbolic</property>
|
||||
<property name="description" translatable="yes">No users matching the search were found.</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwStatusPage" id="error_page">
|
||||
<property name="visible">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="icon-name">dialog-error-symbolic</property>
|
||||
<property name="description" translatable="yes">An error occurred while searching for matches</property>
|
||||
</object>
|
||||
|
@ -137,7 +125,6 @@
|
|||
<property name="spinning">True</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="vexpand">True</property>
|
||||
<style>
|
||||
<class name="session-loading-spinner"/>
|
||||
</style>
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
<property name="activatable">False</property>
|
||||
<property name="selectable">False</property>
|
||||
<property name="child">
|
||||
<object class="ContentMemberRow">
|
||||
<binding name="member">
|
||||
<object class="ContentMemberItemRow">
|
||||
<binding name="item">
|
||||
<lookup name="item">GtkListItem</lookup>
|
||||
</binding>
|
||||
</object>
|
||||
|
|
32
data/resources/ui/content-member-page-list-view.ui
Normal file
32
data/resources/ui/content-member-page-list-view.ui
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<template class="ContentMembersListView" parent="AdwBin">
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="hexpand">True</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="hscrollbar-policy">never</property>
|
||||
<property name="propagate-natural-height">True</property>
|
||||
<property name="child">
|
||||
<object class="AdwClampScrollable">
|
||||
<property name="tightening-threshold">300</property>
|
||||
<property name="maximum-size">400</property>
|
||||
<property name="margin-start">12</property>
|
||||
<property name="margin-end">12</property>
|
||||
<property name="child">
|
||||
<object class="GtkListView" id="members_list_view">
|
||||
<property name="single-click-activate">True</property>
|
||||
<property name="factory">
|
||||
<object class="GtkBuilderListItemFactory">
|
||||
<property name="resource">/org/gnome/Fractal/content-member-item.ui</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<template class="ContentMemberPageMembershipSubpageRow" parent="AdwActionRow">
|
||||
<property name="title" bind-source="ContentMemberPageMembershipSubpageRow" bind-property="label" bind-flags="sync-create"/>
|
||||
<property name="icon-name">system-users-symbolic</property>
|
||||
<property name="activatable">True</property>
|
||||
<property name="margin-top">6</property>
|
||||
<property name="margin-bottom">6</property>
|
||||
<child type="suffix">
|
||||
<object class="GtkLabel" id="members_count">
|
||||
<property name="valign">center</property>
|
||||
<property name="halign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="suffix">
|
||||
<object class="GtkImage">
|
||||
<property name="valign">center</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="icon-name">go-next-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="round-corners"/>
|
||||
</style>
|
||||
</template>
|
||||
</interface>
|
||||
|
|
@ -1,75 +1,79 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<template class="ContentMemberPage" parent="AdwPreferencesPage">
|
||||
<property name="icon-name">system-users-symbolic</property>
|
||||
<property name="title" translatable="yes">Members</property>
|
||||
<property name="name">members</property>
|
||||
<template class="ContentMemberPage" parent="AdwBin">
|
||||
<child>
|
||||
<object class="AdwPreferencesGroup">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="margin-bottom">12</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="member_count">
|
||||
<property name="halign">start</property>
|
||||
<object class="GtkHeaderBar">
|
||||
<child type="start">
|
||||
<object class="GtkButton">
|
||||
<property name="icon-name">go-previous-symbolic</property>
|
||||
<property name="action_name">members.previous</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkToggleButton" id="search_button">
|
||||
<property name="icon-name">system-search-symbolic</property>
|
||||
<accessibility>
|
||||
<property name="label" translatable="yes">Search for Room Members</property>
|
||||
</accessibility>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSearchBar">
|
||||
<property name="search-mode-enabled" bind-source="search_button" bind-property="active"/>
|
||||
<property name="child">
|
||||
<object class="AdwClamp">
|
||||
<property name="hexpand">True</property>
|
||||
<style>
|
||||
<class name="heading"/>
|
||||
<class name="h4"/>
|
||||
</style>
|
||||
<property name="maximum-size">750</property>
|
||||
<property name="tightening-threshold">550</property>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="members_search_entry">
|
||||
<property name="placeholder-text" translatable="yes">Search for room members</property>
|
||||
</object>
|
||||
</child>
|
||||
<accessibility>
|
||||
<property name="label" translatable="yes">Search for room members</property>
|
||||
</accessibility>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkOverlay">
|
||||
<child>
|
||||
<object class="GtkStack" id="list_stack">
|
||||
<property name="transition-type">slide-left</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<child type="overlay">
|
||||
<object class="GtkButton" id="invite_button">
|
||||
<property name="label" translatable="yes">Invite new member</property>
|
||||
<property name="halign">end</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="members_search_entry">
|
||||
<property name="margin-bottom">12</property>
|
||||
<property name="placeholder-text" translatable="yes">Search for room members</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="members_scroll">
|
||||
<property name="propagate-natural-height">True</property>
|
||||
<property name="max-content-height">300</property>
|
||||
<child>
|
||||
<object class="GtkListView" id="members_list_view">
|
||||
<property name="show-separators">True</property>
|
||||
<property name="factory">
|
||||
<object class="GtkBuilderListItemFactory">
|
||||
<property name="resource">/org/gnome/Fractal/content-member-item.ui</property>
|
||||
<property name="valign">end</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="margin-bottom">24</property>
|
||||
<property name="action-name">details.next-page</property>
|
||||
<property name="action-target">'invite'</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="icon-name">system-users-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes">Invite</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
<style>
|
||||
<class name="content"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwPreferencesGroup" id="invited_section">
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="invited_scroll">
|
||||
<property name="propagate-natural-height">True</property>
|
||||
<property name="max-content-height">300</property>
|
||||
<child>
|
||||
<object class="GtkListView" id="invited_list_view">
|
||||
<property name="show-separators">True</property>
|
||||
<property name="factory">
|
||||
<object class="GtkBuilderListItemFactory">
|
||||
<property name="resource">/org/gnome/Fractal/content-member-item.ui</property>
|
||||
</object>
|
||||
</property>
|
||||
<style>
|
||||
<class name="content"/>
|
||||
<class name="pill"/>
|
||||
<class name="suggested-action"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
|
@ -79,3 +83,4 @@
|
|||
</child>
|
||||
</template>
|
||||
</interface>
|
||||
|
||||
|
|
|
@ -1,151 +1,200 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<template class="RoomDetails" parent="AdwPreferencesWindow">
|
||||
<template class="RoomDetails" parent="AdwWindow">
|
||||
<property name="title" translatable="yes">Room Details</property>
|
||||
<property name="default-widget">edit_toggle</property>
|
||||
<property name="search-enabled">False</property>
|
||||
<child>
|
||||
<object class="AdwPreferencesPage">
|
||||
<property name="icon-name">applications-system-symbolic</property>
|
||||
<property name="title" translatable="yes">General</property>
|
||||
<property name="name">general</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="destroy_with_parent">True</property>
|
||||
<property name="default-width">640</property>
|
||||
<property name="default-height">576</property>
|
||||
<property name="content">
|
||||
<object class="GtkStack" id="main_stack">
|
||||
<child>
|
||||
<object class="AdwPreferencesGroup">
|
||||
<style>
|
||||
<class name="room-details-group"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="GtkOverlay">
|
||||
<property name="halign">center</property>
|
||||
<object class="GtkStackPage">
|
||||
<property name="icon-name">applications-system-symbolic</property>
|
||||
<property name="title" translatable="yes">General</property>
|
||||
<property name="name">general</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="ComponentsAvatar">
|
||||
<property name="size">128</property>
|
||||
<binding name="item">
|
||||
<lookup name="avatar">
|
||||
<lookup name="room">RoomDetails</lookup>
|
||||
</lookup>
|
||||
</binding>
|
||||
</object>
|
||||
<object class="GtkHeaderBar"/>
|
||||
</child>
|
||||
<child type="overlay">
|
||||
<object class="AdwBin" id="avatar_remove_button">
|
||||
<style>
|
||||
<class name="cutout-button"/>
|
||||
</style>
|
||||
<property name="halign">end</property>
|
||||
<property name="valign">start</property>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="icon-name">user-trash-symbolic</property>
|
||||
<property name="action-name">details.remove-avatar</property>
|
||||
<style>
|
||||
<class name="circular"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="overlay">
|
||||
<object class="AdwBin" id="avatar_edit_button">
|
||||
<style>
|
||||
<class name="cutout-button"/>
|
||||
</style>
|
||||
<property name="halign">end</property>
|
||||
<property name="valign">end</property>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="icon-name">document-edit-symbolic</property>
|
||||
<property name="action-name">details.choose-avatar</property>
|
||||
<style>
|
||||
<class name="circular"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwClamp">
|
||||
<property name="maximum-size">400</property>
|
||||
<property name="tightening-threshold">400</property>
|
||||
<property name="margin-top">12</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="spacing">6</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkEntry" id="room_name_entry">
|
||||
<property name="sensitive">false</property>
|
||||
<property name="activates-default">True</property>
|
||||
<property name="xalign">0.5</property>
|
||||
<property name="buffer">
|
||||
<object class="GtkEntryBuffer" id="room_name_buffer">
|
||||
<binding name="text">
|
||||
<lookup name="display-name">
|
||||
<lookup name="room">RoomDetails</lookup>
|
||||
</lookup>
|
||||
</binding>
|
||||
</object>
|
||||
</property>
|
||||
<style>
|
||||
<class name="room-details-name"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="room_topic_label">
|
||||
<property name="visible">false</property>
|
||||
<property name="margin-top">12</property>
|
||||
<property name="label" translatable="yes">Description</property>
|
||||
<property name="halign">start</property>
|
||||
<style>
|
||||
<class name="dim-label"/>
|
||||
<class name="caption-heading"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="CustomEntry" id="room_topic_entry">
|
||||
<property name="sensitive">false</property>
|
||||
<property name="margin-bottom">18</property>
|
||||
<child>
|
||||
<object class="AdwClamp">
|
||||
<property name="maximum-size">400</property>
|
||||
<property name="tightening-threshold">400</property>
|
||||
<property name="margin-top">12</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">24</property>
|
||||
<child>
|
||||
<object class="GtkTextView" id="room_topic_text_view">
|
||||
<property name="justification">center</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="accepts-tab">False</property>
|
||||
<property name="top-margin">7</property>
|
||||
<property name="bottom-margin">7</property>
|
||||
<property name="buffer">
|
||||
<object class="GtkTextBuffer" id="room_topic_buffer">
|
||||
<binding name="text">
|
||||
<lookup name="topic">
|
||||
<lookup name="room">RoomDetails</lookup>
|
||||
</lookup>
|
||||
</binding>
|
||||
<object class="AdwPreferencesGroup">
|
||||
<style>
|
||||
<class name="room-details-group"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="GtkOverlay">
|
||||
<property name="halign">center</property>
|
||||
<child>
|
||||
<object class="ComponentsAvatar">
|
||||
<property name="size">128</property>
|
||||
<binding name="item">
|
||||
<lookup name="avatar">
|
||||
<lookup name="room">RoomDetails</lookup>
|
||||
</lookup>
|
||||
</binding>
|
||||
</object>
|
||||
</child>
|
||||
<child type="overlay">
|
||||
<object class="AdwBin" id="avatar_remove_button">
|
||||
<style>
|
||||
<class name="cutout-button"/>
|
||||
</style>
|
||||
<property name="halign">end</property>
|
||||
<property name="valign">start</property>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="icon-name">user-trash-symbolic</property>
|
||||
<property name="action-name">details.remove-avatar</property>
|
||||
<style>
|
||||
<class name="circular"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="overlay">
|
||||
<object class="AdwBin" id="avatar_edit_button">
|
||||
<style>
|
||||
<class name="cutout-button"/>
|
||||
</style>
|
||||
<property name="halign">end</property>
|
||||
<property name="valign">end</property>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="icon-name">document-edit-symbolic</property>
|
||||
<property name="action-name">details.choose-avatar</property>
|
||||
<style>
|
||||
<class name="circular"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="spacing">6</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkEntry" id="room_name_entry">
|
||||
<property name="sensitive">false</property>
|
||||
<property name="activates-default">True</property>
|
||||
<property name="xalign">0.5</property>
|
||||
<property name="buffer">
|
||||
<object class="GtkEntryBuffer" id="room_name_buffer">
|
||||
<binding name="text">
|
||||
<lookup name="display-name">
|
||||
<lookup name="room">RoomDetails</lookup>
|
||||
</lookup>
|
||||
</binding>
|
||||
</object>
|
||||
</property>
|
||||
<style>
|
||||
<class name="room-details-name"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="room_topic_label">
|
||||
<property name="visible">false</property>
|
||||
<property name="margin-top">12</property>
|
||||
<property name="label" translatable="yes">Description</property>
|
||||
<property name="halign">start</property>
|
||||
<style>
|
||||
<class name="dim-label"/>
|
||||
<class name="caption-heading"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="CustomEntry" id="room_topic_entry">
|
||||
<property name="sensitive">false</property>
|
||||
<property name="margin-bottom">18</property>
|
||||
<child>
|
||||
<object class="GtkTextView" id="room_topic_text_view">
|
||||
<property name="justification">center</property>
|
||||
<property name="wrap-mode">word-char</property>
|
||||
<property name="accepts-tab">False</property>
|
||||
<property name="top-margin">7</property>
|
||||
<property name="bottom-margin">7</property>
|
||||
<property name="buffer">
|
||||
<object class="GtkTextBuffer" id="room_topic_buffer">
|
||||
<binding name="text">
|
||||
<lookup name="topic">
|
||||
<lookup name="room">RoomDetails</lookup>
|
||||
</lookup>
|
||||
</binding>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="room-details-topic"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="edit_toggle">
|
||||
<property name="halign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwPreferencesGroup">
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="title" translatable="yes">Members</property>
|
||||
<property name="icon-name">system-users-symbolic</property>
|
||||
<property name="action-name">details.next-page</property>
|
||||
<property name="action-target">'members'</property>
|
||||
<property name="activatable">True</property>
|
||||
<child type="suffix">
|
||||
<object class="GtkLabel" id="members_count">
|
||||
<property name="valign">center</property>
|
||||
<property name="halign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="suffix">
|
||||
<object class="GtkImage">
|
||||
<property name="valign">center</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="icon-name">go-next-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="room-details-topic"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="edit_toggle">
|
||||
<property name="halign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<!-- ContentMemberPage goes here -->
|
||||
</property>
|
||||
<style>
|
||||
<class name="room-details"/>
|
||||
</style>
|
||||
</template>
|
||||
</interface>
|
||||
|
||||
|
|
|
@ -70,6 +70,7 @@ src/session/content/verification/identity_verification_widget.rs
|
|||
src/session/content/verification/session_verification.rs
|
||||
src/session/mod.rs
|
||||
src/session/room/event_actions.rs
|
||||
src/session/room/member.rs
|
||||
src/session/room/member_role.rs
|
||||
src/session/room/mod.rs
|
||||
src/session/room/timeline/timeline_day_divider.rs
|
||||
|
|
|
@ -11,7 +11,7 @@ use self::{
|
|||
};
|
||||
use crate::{
|
||||
components::{Pill, SpinnerButton},
|
||||
session::{content::RoomDetails, Room, User},
|
||||
session::{Room, User},
|
||||
spawn,
|
||||
};
|
||||
|
||||
|
@ -246,8 +246,7 @@ impl InviteSubpage {
|
|||
}
|
||||
|
||||
fn close(&self) {
|
||||
let window = self.root().unwrap().downcast::<RoomDetails>().unwrap();
|
||||
window.close_invite_subpage();
|
||||
self.activate_action("details.previous-page", None).unwrap();
|
||||
}
|
||||
|
||||
fn add_user_pill(&self, user: &Invitee) {
|
||||
|
|
|
@ -0,0 +1,233 @@
|
|||
use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
|
||||
|
||||
use crate::session::content::room_details::member_page::MembershipSubpageItem;
|
||||
|
||||
mod imp {
|
||||
use std::cell::Cell;
|
||||
|
||||
use once_cell::{sync::Lazy, unsync::OnceCell};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ExtraLists {
|
||||
pub joined: OnceCell<gio::ListModel>,
|
||||
pub invited: OnceCell<MembershipSubpageItem>,
|
||||
pub banned: OnceCell<MembershipSubpageItem>,
|
||||
pub invited_is_empty: Cell<bool>,
|
||||
pub banned_is_empty: Cell<bool>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for ExtraLists {
|
||||
const NAME: &'static str = "ContentMembersExtraLists";
|
||||
type Type = super::ExtraLists;
|
||||
type Interfaces = (gio::ListModel,);
|
||||
}
|
||||
|
||||
impl ObjectImpl for ExtraLists {
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||
vec![
|
||||
glib::ParamSpecObject::new(
|
||||
"joined",
|
||||
"Joined",
|
||||
"The item for the subpage of joined members",
|
||||
gio::ListModel::static_type(),
|
||||
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
|
||||
),
|
||||
glib::ParamSpecObject::new(
|
||||
"invited",
|
||||
"Invited",
|
||||
"The item for the subpage of invited members",
|
||||
MembershipSubpageItem::static_type(),
|
||||
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
|
||||
),
|
||||
glib::ParamSpecObject::new(
|
||||
"banned",
|
||||
"Banned",
|
||||
"The item for the subpage of banned members",
|
||||
MembershipSubpageItem::static_type(),
|
||||
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
|
||||
),
|
||||
]
|
||||
});
|
||||
|
||||
PROPERTIES.as_ref()
|
||||
}
|
||||
|
||||
fn set_property(
|
||||
&self,
|
||||
obj: &Self::Type,
|
||||
_id: usize,
|
||||
value: &glib::Value,
|
||||
pspec: &glib::ParamSpec,
|
||||
) {
|
||||
match pspec.name() {
|
||||
"joined" => obj.set_joined(value.get().unwrap()),
|
||||
"invited" => obj.set_invited(value.get().unwrap()),
|
||||
"banned" => obj.set_banned(value.get().unwrap()),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
match pspec.name() {
|
||||
"joined" => obj.joined().to_value(),
|
||||
"invited" => obj.invited().to_value(),
|
||||
"banned" => obj.banned().to_value(),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn constructed(&self, obj: &Self::Type) {
|
||||
self.parent_constructed(obj);
|
||||
|
||||
let joined_members = obj.joined();
|
||||
let invited_members = obj.invited().model();
|
||||
let banned_members = obj.banned().model();
|
||||
|
||||
joined_members.connect_items_changed(
|
||||
clone!(@weak obj => move |_, position, removed, added| {
|
||||
obj.items_changed(position + obj.n_visible_extras(), removed, added)
|
||||
}),
|
||||
);
|
||||
|
||||
invited_members.connect_items_changed(clone!(@weak obj => move |_, _, _, _| {
|
||||
obj.update_items();
|
||||
}));
|
||||
|
||||
banned_members.connect_items_changed(clone!(@weak obj => move |_, _, _, _| {
|
||||
obj.update_items();
|
||||
}));
|
||||
|
||||
self.invited_is_empty.set(invited_members.n_items() == 0);
|
||||
self.banned_is_empty.set(banned_members.n_items() == 0);
|
||||
}
|
||||
}
|
||||
|
||||
impl ListModelImpl for ExtraLists {
|
||||
fn item_type(&self, _list_model: &Self::Type) -> glib::Type {
|
||||
glib::Object::static_type()
|
||||
}
|
||||
|
||||
fn n_items(&self, list_model: &Self::Type) -> u32 {
|
||||
list_model.joined().n_items() + list_model.n_visible_extras()
|
||||
}
|
||||
|
||||
fn item(&self, list_model: &Self::Type, position: u32) -> Option<glib::Object> {
|
||||
if position == 0 && !self.invited_is_empty.get() {
|
||||
let invited = self.invited.get().unwrap();
|
||||
return Some(invited.clone().upcast());
|
||||
}
|
||||
|
||||
if (position == 0 && self.invited_is_empty.get() && !self.banned_is_empty.get())
|
||||
|| (position == 1 && !self.banned_is_empty.get())
|
||||
{
|
||||
let banned = self.banned.get().unwrap();
|
||||
return Some(banned.clone().upcast());
|
||||
}
|
||||
|
||||
list_model
|
||||
.joined()
|
||||
.item(position - list_model.n_visible_extras())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct ExtraLists(ObjectSubclass<imp::ExtraLists>)
|
||||
@implements gio::ListModel;
|
||||
}
|
||||
|
||||
impl ExtraLists {
|
||||
pub fn new(
|
||||
joined: &impl IsA<gio::ListModel>,
|
||||
invited: &MembershipSubpageItem,
|
||||
banned: &MembershipSubpageItem,
|
||||
) -> Self {
|
||||
glib::Object::new(&[("joined", joined), ("invited", invited), ("banned", banned)])
|
||||
.expect("Failed to create ExtraLists")
|
||||
}
|
||||
|
||||
pub fn joined(&self) -> &gio::ListModel {
|
||||
self.imp().joined.get().unwrap()
|
||||
}
|
||||
|
||||
fn set_joined(&self, model: gio::ListModel) {
|
||||
self.imp().joined.set(model).unwrap();
|
||||
}
|
||||
|
||||
pub fn invited(&self) -> &MembershipSubpageItem {
|
||||
self.imp().invited.get().unwrap()
|
||||
}
|
||||
|
||||
fn set_invited(&self, item: MembershipSubpageItem) {
|
||||
self.imp().invited.set(item).unwrap();
|
||||
}
|
||||
|
||||
pub fn banned(&self) -> &MembershipSubpageItem {
|
||||
self.imp().banned.get().unwrap()
|
||||
}
|
||||
|
||||
fn set_banned(&self, item: MembershipSubpageItem) {
|
||||
self.imp().banned.set(item).unwrap();
|
||||
}
|
||||
|
||||
fn update_items(&self) {
|
||||
let priv_ = self.imp();
|
||||
|
||||
let invited_was_empty = priv_.invited_is_empty.get();
|
||||
let banned_was_empty = priv_.banned_is_empty.get();
|
||||
|
||||
let invited_is_empty = self.invited().model().n_items() == 0;
|
||||
let banned_is_empty = self.banned().model().n_items() == 0;
|
||||
|
||||
let invited_changed = invited_was_empty != invited_is_empty;
|
||||
let banned_changed = banned_was_empty != banned_is_empty;
|
||||
|
||||
if !invited_changed && !banned_changed {
|
||||
// Nothing changed so don't do anything
|
||||
return;
|
||||
}
|
||||
|
||||
let mut position = 0;
|
||||
let mut removed = 0;
|
||||
let mut added = 0;
|
||||
|
||||
if invited_changed {
|
||||
if invited_is_empty {
|
||||
removed = 1;
|
||||
} else {
|
||||
added = 1;
|
||||
}
|
||||
} else if !invited_is_empty {
|
||||
position = 1;
|
||||
}
|
||||
|
||||
if banned_changed {
|
||||
if banned_is_empty {
|
||||
removed += 1;
|
||||
} else {
|
||||
added += 1;
|
||||
}
|
||||
}
|
||||
|
||||
priv_.invited_is_empty.set(invited_is_empty);
|
||||
priv_.banned_is_empty.set(banned_is_empty);
|
||||
|
||||
self.items_changed(position, removed, added);
|
||||
}
|
||||
|
||||
fn n_visible_extras(&self) -> u32 {
|
||||
let priv_ = self.imp();
|
||||
let mut len = 0;
|
||||
if !priv_.invited_is_empty.get() {
|
||||
len += 1;
|
||||
}
|
||||
if !priv_.banned_is_empty.get() {
|
||||
len += 1;
|
||||
}
|
||||
len
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
use adw::{prelude::BinExt, subclass::prelude::*};
|
||||
use gtk::{glib, glib::prelude::*};
|
||||
|
||||
use super::{MemberRow, MembershipSubpageItem, MembershipSubpageRow};
|
||||
use crate::session::room::Member;
|
||||
|
||||
mod imp {
|
||||
use std::cell::RefCell;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ItemRow {
|
||||
pub item: RefCell<Option<glib::Object>>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for ItemRow {
|
||||
const NAME: &'static str = "ContentMemberItemRow";
|
||||
type Type = super::ItemRow;
|
||||
type ParentType = adw::Bin;
|
||||
}
|
||||
|
||||
impl ObjectImpl for ItemRow {
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
use once_cell::sync::Lazy;
|
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||
vec![glib::ParamSpecObject::new(
|
||||
"item",
|
||||
"Item",
|
||||
"The membership subpage item represented by this row",
|
||||
glib::Object::static_type(),
|
||||
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
|
||||
)]
|
||||
});
|
||||
|
||||
PROPERTIES.as_ref()
|
||||
}
|
||||
|
||||
fn set_property(
|
||||
&self,
|
||||
obj: &Self::Type,
|
||||
_id: usize,
|
||||
value: &glib::Value,
|
||||
pspec: &glib::ParamSpec,
|
||||
) {
|
||||
match pspec.name() {
|
||||
"item" => obj.set_item(value.get().unwrap()),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
match pspec.name() {
|
||||
"item" => obj.item().to_value(),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for ItemRow {}
|
||||
impl BinImpl for ItemRow {}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct ItemRow(ObjectSubclass<imp::ItemRow>)
|
||||
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
|
||||
}
|
||||
|
||||
impl ItemRow {
|
||||
pub fn new() -> Self {
|
||||
glib::Object::new(&[]).expect("Failed to create ItemRow")
|
||||
}
|
||||
|
||||
pub fn item(&self) -> Option<glib::Object> {
|
||||
self.imp().item.borrow().clone()
|
||||
}
|
||||
|
||||
fn set_item(&self, item: Option<glib::Object>) {
|
||||
if self.item() == item {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(item) = item.as_ref() {
|
||||
if let Some(member) = item.downcast_ref::<Member>() {
|
||||
let child = if let Some(Ok(child)) = self.child().map(|w| w.downcast::<MemberRow>())
|
||||
{
|
||||
child
|
||||
} else {
|
||||
let child = MemberRow::new();
|
||||
self.set_child(Some(&child));
|
||||
child
|
||||
};
|
||||
child.set_member(Some(member.clone()));
|
||||
} else if let Some(item) = item.downcast_ref::<MembershipSubpageItem>() {
|
||||
let child = if let Some(Ok(child)) =
|
||||
self.child().map(|w| w.downcast::<MembershipSubpageRow>())
|
||||
{
|
||||
child
|
||||
} else {
|
||||
let child = MembershipSubpageRow::new();
|
||||
self.set_child(Some(&child));
|
||||
child
|
||||
};
|
||||
|
||||
child.set_item(Some(item.clone()));
|
||||
} else {
|
||||
unimplemented!("The object {:?} doesn't have a widget implementation", item);
|
||||
}
|
||||
}
|
||||
|
||||
self.imp().item.replace(item);
|
||||
self.notify("item");
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ItemRow {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
|
@ -1,7 +1,10 @@
|
|||
use adw::subclass::prelude::BinImpl;
|
||||
use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate};
|
||||
|
||||
use crate::session::{content::RoomDetails, room::Member};
|
||||
use crate::session::{
|
||||
content::room_details::{member_page::MemberMenu, MemberPage},
|
||||
room::Member,
|
||||
};
|
||||
|
||||
mod imp {
|
||||
use std::cell::RefCell;
|
||||
|
@ -76,10 +79,8 @@ mod imp {
|
|||
|
||||
self.menu_btn
|
||||
.connect_toggled(clone!(@weak obj => move |btn| {
|
||||
if let Some(details) = obj.details() {
|
||||
let page = details.member_page();
|
||||
let menu = page.member_menu();
|
||||
if btn.is_active() {
|
||||
if btn.is_active() {
|
||||
if let Some(menu) = obj.member_menu() {
|
||||
menu.present_popover(btn, obj.member());
|
||||
}
|
||||
}
|
||||
|
@ -96,8 +97,8 @@ glib::wrapper! {
|
|||
}
|
||||
|
||||
impl MemberRow {
|
||||
pub fn new(member: &Member) -> Self {
|
||||
glib::Object::new(&[("member", member)]).expect("Failed to create MemberRow")
|
||||
pub fn new() -> Self {
|
||||
glib::Object::new(&[]).expect("Failed to create MemberRow")
|
||||
}
|
||||
|
||||
pub fn member(&self) -> Option<Member> {
|
||||
|
@ -113,10 +114,7 @@ impl MemberRow {
|
|||
|
||||
// We need to update the member of the menu if it's shown for this row
|
||||
if priv_.menu_btn.is_active() {
|
||||
if let Some(details) = self.details() {
|
||||
let page = details.member_page();
|
||||
let menu = page.member_menu();
|
||||
|
||||
if let Some(menu) = self.member_menu() {
|
||||
menu.set_member(member.clone());
|
||||
}
|
||||
}
|
||||
|
@ -125,7 +123,17 @@ impl MemberRow {
|
|||
self.notify("member");
|
||||
}
|
||||
|
||||
fn details(&self) -> Option<RoomDetails> {
|
||||
Some(self.root()?.downcast::<RoomDetails>().unwrap())
|
||||
fn member_menu(&self) -> Option<MemberMenu> {
|
||||
let member_page = self
|
||||
.ancestor(MemberPage::static_type())?
|
||||
.downcast::<MemberPage>()
|
||||
.unwrap();
|
||||
Some(member_page.member_menu().clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MemberRow {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
use gtk::{
|
||||
gio, glib,
|
||||
glib::{prelude::*, subclass::prelude::*},
|
||||
};
|
||||
|
||||
use crate::session::room::Membership;
|
||||
|
||||
mod imp {
|
||||
use std::cell::Cell;
|
||||
|
||||
use once_cell::{sync::Lazy, unsync::OnceCell};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct MembershipSubpageItem {
|
||||
pub state: Cell<Membership>,
|
||||
pub model: OnceCell<gio::ListModel>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for MembershipSubpageItem {
|
||||
const NAME: &'static str = "ContentMemberPageMembershipSubpageItem";
|
||||
type Type = super::MembershipSubpageItem;
|
||||
}
|
||||
|
||||
impl ObjectImpl for MembershipSubpageItem {
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||
vec![
|
||||
glib::ParamSpecEnum::new(
|
||||
"state",
|
||||
"State",
|
||||
"The membership state this list contains",
|
||||
Membership::static_type(),
|
||||
Membership::default() as i32,
|
||||
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
|
||||
),
|
||||
glib::ParamSpecObject::new(
|
||||
"model",
|
||||
"Model",
|
||||
"The model used for this subview",
|
||||
gio::ListModel::static_type(),
|
||||
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
|
||||
),
|
||||
]
|
||||
});
|
||||
|
||||
PROPERTIES.as_ref()
|
||||
}
|
||||
|
||||
fn set_property(
|
||||
&self,
|
||||
obj: &Self::Type,
|
||||
_id: usize,
|
||||
value: &glib::Value,
|
||||
pspec: &glib::ParamSpec,
|
||||
) {
|
||||
match pspec.name() {
|
||||
"state" => obj.set_state(value.get().unwrap()),
|
||||
"model" => obj.set_model(value.get().unwrap()),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
match pspec.name() {
|
||||
"state" => obj.state().to_value(),
|
||||
"model" => obj.model().to_value(),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct MembershipSubpageItem(ObjectSubclass<imp::MembershipSubpageItem>);
|
||||
}
|
||||
|
||||
impl MembershipSubpageItem {
|
||||
pub fn new(state: Membership, model: &impl IsA<gio::ListModel>) -> Self {
|
||||
glib::Object::new(&[("state", &state), ("model", model)])
|
||||
.expect("Failed to create MembershipSubpageItem")
|
||||
}
|
||||
|
||||
pub fn state(&self) -> Membership {
|
||||
self.imp().state.get()
|
||||
}
|
||||
|
||||
fn set_state(&self, state: Membership) {
|
||||
self.imp().state.set(state);
|
||||
}
|
||||
|
||||
pub fn model(&self) -> &gio::ListModel {
|
||||
self.imp().model.get().unwrap()
|
||||
}
|
||||
|
||||
fn set_model(&self, model: gio::ListModel) {
|
||||
self.imp().model.set(model).unwrap();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
use adw::subclass::prelude::*;
|
||||
use gtk::{gdk, glib, glib::clone, prelude::*, CompositeTemplate};
|
||||
|
||||
use crate::session::content::room_details::member_page::MembershipSubpageItem;
|
||||
|
||||
mod imp {
|
||||
use std::cell::RefCell;
|
||||
|
||||
use glib::{signal::SignalHandlerId, subclass::InitializingObject};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default, CompositeTemplate)]
|
||||
#[template(resource = "/org/gnome/Fractal/content-member-page-membership-subpage-row.ui")]
|
||||
pub struct MembershipSubpageRow {
|
||||
/// The item of this row.
|
||||
pub item: RefCell<Option<MembershipSubpageItem>>,
|
||||
pub gesture: gtk::GestureClick,
|
||||
#[template_child]
|
||||
pub members_count: TemplateChild<gtk::Label>,
|
||||
pub members_count_handler_id: RefCell<Option<SignalHandlerId>>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for MembershipSubpageRow {
|
||||
const NAME: &'static str = "ContentMemberPageMembershipSubpageRow";
|
||||
type Type = super::MembershipSubpageRow;
|
||||
type ParentType = adw::ActionRow;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
Self::bind_template(klass);
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for MembershipSubpageRow {
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
use once_cell::sync::Lazy;
|
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||
vec![
|
||||
glib::ParamSpecObject::new(
|
||||
"item",
|
||||
"Item",
|
||||
"The item of this row",
|
||||
MembershipSubpageItem::static_type(),
|
||||
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
|
||||
),
|
||||
glib::ParamSpecString::new(
|
||||
"label",
|
||||
"Label",
|
||||
"The label to show for this row",
|
||||
None,
|
||||
glib::ParamFlags::READABLE,
|
||||
),
|
||||
]
|
||||
});
|
||||
|
||||
PROPERTIES.as_ref()
|
||||
}
|
||||
|
||||
fn set_property(
|
||||
&self,
|
||||
obj: &Self::Type,
|
||||
_id: usize,
|
||||
value: &glib::Value,
|
||||
pspec: &glib::ParamSpec,
|
||||
) {
|
||||
match pspec.name() {
|
||||
"item" => obj.set_item(value.get().unwrap()),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
match pspec.name() {
|
||||
"item" => obj.item().to_value(),
|
||||
"label" => obj.label().to_value(),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn constructed(&self, obj: &Self::Type) {
|
||||
self.parent_constructed(obj);
|
||||
|
||||
self.gesture.set_touch_only(false);
|
||||
self.gesture.set_button(gdk::BUTTON_PRIMARY);
|
||||
|
||||
self.gesture
|
||||
.connect_released(clone!(@weak obj => move |_, _, _, _| {
|
||||
if let Some(item) = obj.item() {
|
||||
obj.activate_action(
|
||||
"members.subpage",
|
||||
Some(&(item.state() as u32).to_variant()),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}));
|
||||
|
||||
self.gesture
|
||||
.set_propagation_phase(gtk::PropagationPhase::Capture);
|
||||
obj.add_controller(&self.gesture);
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for MembershipSubpageRow {}
|
||||
impl ListBoxRowImpl for MembershipSubpageRow {}
|
||||
impl PreferencesRowImpl for MembershipSubpageRow {}
|
||||
impl ActionRowImpl for MembershipSubpageRow {}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct MembershipSubpageRow(ObjectSubclass<imp::MembershipSubpageRow>)
|
||||
@extends gtk::Widget, adw::ActionRow, @implements gtk::Accessible;
|
||||
}
|
||||
|
||||
impl MembershipSubpageRow {
|
||||
pub fn new() -> Self {
|
||||
glib::Object::new(&[]).expect("Failed to create MembershipSubpageRow")
|
||||
}
|
||||
|
||||
pub fn item(&self) -> Option<MembershipSubpageItem> {
|
||||
self.imp().item.borrow().clone()
|
||||
}
|
||||
|
||||
pub fn set_item(&self, item: Option<MembershipSubpageItem>) {
|
||||
let priv_ = self.imp();
|
||||
let prev_item = self.item();
|
||||
|
||||
if prev_item == item {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(signal_id) = priv_.members_count_handler_id.take() {
|
||||
if let Some(prev_item) = prev_item {
|
||||
prev_item.disconnect(signal_id);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(item) = item.as_ref() {
|
||||
let model = item.model();
|
||||
let signal_id =
|
||||
model.connect_items_changed(clone!(@weak self as obj => move |model, _, _, _| {
|
||||
obj.member_count_changed(model.n_items());
|
||||
}));
|
||||
|
||||
self.member_count_changed(model.n_items());
|
||||
|
||||
self.imp().members_count_handler_id.replace(Some(signal_id));
|
||||
}
|
||||
|
||||
self.imp().item.replace(item);
|
||||
self.notify("item");
|
||||
self.notify("label");
|
||||
}
|
||||
|
||||
pub fn label(&self) -> Option<String> {
|
||||
Some(self.item()?.state().to_string())
|
||||
}
|
||||
|
||||
fn member_count_changed(&self, n: u32) {
|
||||
self.imp().members_count.set_text(&format!("{}", n));
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MembershipSubpageRow {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
use adw::{
|
||||
prelude::*,
|
||||
subclass::{bin::BinImpl, prelude::*},
|
||||
};
|
||||
use gtk::{gio, glib, CompositeTemplate};
|
||||
|
||||
use crate::components::{Avatar, Badge};
|
||||
|
||||
pub mod extra_lists;
|
||||
mod item_row;
|
||||
mod member_row;
|
||||
mod membership_subpage_item;
|
||||
mod membership_subpage_row;
|
||||
|
||||
use item_row::ItemRow;
|
||||
use member_row::MemberRow;
|
||||
pub use membership_subpage_item::MembershipSubpageItem;
|
||||
use membership_subpage_row::MembershipSubpageRow;
|
||||
|
||||
mod imp {
|
||||
use glib::subclass::InitializingObject;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default, CompositeTemplate)]
|
||||
#[template(resource = "/org/gnome/Fractal/content-member-page-list-view.ui")]
|
||||
pub struct MembersListView {
|
||||
#[template_child]
|
||||
pub members_list_view: TemplateChild<gtk::ListView>,
|
||||
pub model: glib::WeakRef<gio::ListModel>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for MembersListView {
|
||||
const NAME: &'static str = "ContentMembersListView";
|
||||
type Type = super::MembersListView;
|
||||
type ParentType = adw::Bin;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
Avatar::static_type();
|
||||
Badge::static_type();
|
||||
MemberRow::static_type();
|
||||
ItemRow::static_type();
|
||||
Self::bind_template(klass);
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for MembersListView {
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||
vec![glib::ParamSpecObject::new(
|
||||
"model",
|
||||
"Model",
|
||||
"The model used for this view",
|
||||
gio::ListModel::static_type(),
|
||||
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
|
||||
)]
|
||||
});
|
||||
|
||||
PROPERTIES.as_ref()
|
||||
}
|
||||
|
||||
fn set_property(
|
||||
&self,
|
||||
obj: &Self::Type,
|
||||
_id: usize,
|
||||
value: &glib::Value,
|
||||
pspec: &glib::ParamSpec,
|
||||
) {
|
||||
match pspec.name() {
|
||||
"model" => obj.set_model(value.get::<Option<&gio::ListModel>>().unwrap()),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
match pspec.name() {
|
||||
"model" => obj.model().to_value(),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl WidgetImpl for MembersListView {}
|
||||
impl BinImpl for MembersListView {}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct MembersListView(ObjectSubclass<imp::MembersListView>)
|
||||
@extends gtk::Widget, adw::Bin;
|
||||
}
|
||||
|
||||
impl MembersListView {
|
||||
pub fn new(model: &impl IsA<gio::ListModel>) -> Self {
|
||||
glib::Object::new(&[("model", model)]).expect("Failed to create MembersListView")
|
||||
}
|
||||
|
||||
pub fn model(&self) -> Option<gio::ListModel> {
|
||||
self.imp().model.upgrade()
|
||||
}
|
||||
|
||||
pub fn set_model(&self, model: Option<&impl IsA<gio::ListModel>>) {
|
||||
let model: Option<&gio::ListModel> = model.map(|model| model.upcast_ref());
|
||||
if self.model().as_ref() == model {
|
||||
return;
|
||||
}
|
||||
|
||||
self.imp()
|
||||
.members_list_view
|
||||
.set_model(Some(>k::NoSelection::new(model)));
|
||||
|
||||
self.imp().model.set(model);
|
||||
self.notify("model");
|
||||
}
|
||||
}
|
|
@ -1,29 +1,37 @@
|
|||
use adw::{prelude::*, subclass::prelude::*};
|
||||
use adw::{
|
||||
prelude::*,
|
||||
subclass::{bin::BinImpl, prelude::*},
|
||||
};
|
||||
use gettextrs::gettext;
|
||||
use gtk::{
|
||||
gio,
|
||||
glib::{self, clone, closure},
|
||||
CompositeTemplate,
|
||||
};
|
||||
use log::warn;
|
||||
|
||||
mod member_menu;
|
||||
mod member_row;
|
||||
mod members_list_view;
|
||||
|
||||
use self::{member_menu::MemberMenu, member_row::MemberRow};
|
||||
use members_list_view::{MembersListView, MembershipSubpageItem};
|
||||
|
||||
use self::member_menu::MemberMenu;
|
||||
use crate::{
|
||||
components::{Avatar, Badge},
|
||||
ngettext_f,
|
||||
prelude::*,
|
||||
session::{
|
||||
content::RoomDetails,
|
||||
content::room_details::member_page::members_list_view::extra_lists::ExtraLists,
|
||||
room::{Member, Membership, RoomAction},
|
||||
Room, User, UserActions,
|
||||
},
|
||||
spawn,
|
||||
};
|
||||
|
||||
const MAX_LIST_HEIGHT: i32 = 300;
|
||||
|
||||
mod imp {
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
collections::HashMap,
|
||||
};
|
||||
|
||||
use glib::subclass::InitializingObject;
|
||||
use once_cell::{sync::Lazy, unsync::OnceCell};
|
||||
|
||||
|
@ -32,36 +40,27 @@ mod imp {
|
|||
#[derive(Debug, Default, CompositeTemplate)]
|
||||
#[template(resource = "/org/gnome/Fractal/content-member-page.ui")]
|
||||
pub struct MemberPage {
|
||||
pub room: OnceCell<Room>,
|
||||
#[template_child]
|
||||
pub member_count: TemplateChild<gtk::Label>,
|
||||
#[template_child]
|
||||
pub invite_button: TemplateChild<gtk::Button>,
|
||||
pub room: RefCell<Option<Room>>,
|
||||
#[template_child]
|
||||
pub members_search_entry: TemplateChild<gtk::SearchEntry>,
|
||||
#[template_child]
|
||||
pub members_list_view: TemplateChild<gtk::ListView>,
|
||||
pub list_stack: TemplateChild<gtk::Stack>,
|
||||
#[template_child]
|
||||
pub members_scroll: TemplateChild<gtk::ScrolledWindow>,
|
||||
pub invite_button: TemplateChild<gtk::Button>,
|
||||
pub member_menu: OnceCell<MemberMenu>,
|
||||
#[template_child]
|
||||
pub invited_section: TemplateChild<adw::PreferencesGroup>,
|
||||
#[template_child]
|
||||
pub invited_list_view: TemplateChild<gtk::ListView>,
|
||||
#[template_child]
|
||||
pub invited_scroll: TemplateChild<gtk::ScrolledWindow>,
|
||||
pub list_stack_children: RefCell<HashMap<Membership, glib::WeakRef<MembersListView>>>,
|
||||
pub state: Cell<Membership>,
|
||||
pub invite_action_watch: RefCell<Option<gtk::ExpressionWatch>>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for MemberPage {
|
||||
const NAME: &'static str = "ContentMemberPage";
|
||||
type Type = super::MemberPage;
|
||||
type ParentType = adw::PreferencesPage;
|
||||
type ParentType = adw::Bin;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
Avatar::static_type();
|
||||
Badge::static_type();
|
||||
MemberRow::static_type();
|
||||
MembersListView::static_type();
|
||||
Self::bind_template(klass);
|
||||
|
||||
klass.install_action("member.verify", None, move |widget, _, _| {
|
||||
|
@ -71,6 +70,28 @@ mod imp {
|
|||
warn!("No member was selected to be verified");
|
||||
}
|
||||
});
|
||||
|
||||
klass.install_action("members.subpage", Some("u"), move |widget, _, param| {
|
||||
use std::convert::TryFrom;
|
||||
|
||||
let state = param
|
||||
.and_then(|variant| variant.get::<u32>())
|
||||
.and_then(|u| Membership::try_from(u).ok());
|
||||
|
||||
if let Some(state) = state {
|
||||
widget.set_state(state);
|
||||
}
|
||||
});
|
||||
|
||||
klass.install_action("members.previous", None, move |widget, _, _| {
|
||||
if widget.state() == Membership::Join {
|
||||
widget
|
||||
.activate_action("details.previous-page", None)
|
||||
.unwrap();
|
||||
} else {
|
||||
widget.set_state(Membership::Join);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
|
@ -87,7 +108,7 @@ mod imp {
|
|||
"Room",
|
||||
"The room backing all details of the member page",
|
||||
Room::static_type(),
|
||||
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
|
||||
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
|
||||
),
|
||||
glib::ParamSpecObject::new(
|
||||
"member-menu",
|
||||
|
@ -96,6 +117,14 @@ mod imp {
|
|||
MemberMenu::static_type(),
|
||||
glib::ParamFlags::READABLE,
|
||||
),
|
||||
glib::ParamSpecEnum::new(
|
||||
"state",
|
||||
"State",
|
||||
"The membership state of the displayed members",
|
||||
Membership::static_type(),
|
||||
Membership::default() as i32,
|
||||
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
|
||||
),
|
||||
]
|
||||
});
|
||||
|
||||
|
@ -111,33 +140,35 @@ mod imp {
|
|||
) {
|
||||
match pspec.name() {
|
||||
"room" => obj.set_room(value.get().unwrap()),
|
||||
"state" => obj.set_state(value.get().unwrap()),
|
||||
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
match pspec.name() {
|
||||
"room" => self.room.get().to_value(),
|
||||
"room" => obj.room().to_value(),
|
||||
"member-menu" => obj.member_menu().to_value(),
|
||||
"state" => obj.state().to_value(),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn constructed(&self, obj: &Self::Type) {
|
||||
self.parent_constructed(obj);
|
||||
|
||||
obj.init_members_list();
|
||||
obj.init_invited_list();
|
||||
obj.init_invite_button();
|
||||
fn dispose(&self, _: &Self::Type) {
|
||||
if let Some(invite_action) = self.invite_action_watch.take() {
|
||||
invite_action.unwatch();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for MemberPage {}
|
||||
impl PreferencesPageImpl for MemberPage {}
|
||||
impl BinImpl for MemberPage {}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct MemberPage(ObjectSubclass<imp::MemberPage>)
|
||||
@extends gtk::Widget, adw::PreferencesPage;
|
||||
@extends gtk::Widget, adw::Bin;
|
||||
}
|
||||
|
||||
impl MemberPage {
|
||||
|
@ -145,35 +176,34 @@ impl MemberPage {
|
|||
glib::Object::new(&[("room", room)]).expect("Failed to create MemberPage")
|
||||
}
|
||||
|
||||
pub fn room(&self) -> &Room {
|
||||
self.imp().room.get().unwrap()
|
||||
pub fn room(&self) -> Option<Room> {
|
||||
self.imp().room.borrow().as_ref().cloned()
|
||||
}
|
||||
|
||||
fn set_room(&self, room: Room) {
|
||||
self.imp().room.set(room).expect("Room already initialized");
|
||||
}
|
||||
|
||||
fn init_members_list(&self) {
|
||||
pub fn set_room(&self, room: Option<Room>) {
|
||||
let priv_ = self.imp();
|
||||
let members = self.room().members();
|
||||
let prev_room = self.room();
|
||||
|
||||
// Only keep the members that are in the join membership state
|
||||
let joined_expression = gtk::PropertyExpression::new(
|
||||
Member::static_type(),
|
||||
gtk::Expression::NONE,
|
||||
"membership",
|
||||
)
|
||||
.chain_closure::<bool>(closure!(
|
||||
|_: Option<glib::Object>, membership: Membership| { membership == Membership::Join }
|
||||
));
|
||||
let joined_filter = gtk::BoolFilter::new(Some(joined_expression));
|
||||
let joined_members = gtk::FilterListModel::new(Some(members), Some(&joined_filter));
|
||||
if prev_room == room {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set up the members count.
|
||||
self.member_count_changed(joined_members.n_items());
|
||||
joined_members.connect_items_changed(clone!(@weak self as obj => move |members, _, _, _| {
|
||||
obj.member_count_changed(members.n_items());
|
||||
}));
|
||||
if let Some(invite_action) = priv_.invite_action_watch.take() {
|
||||
invite_action.unwatch();
|
||||
}
|
||||
|
||||
if let Some(room) = room.as_ref() {
|
||||
self.init_members_list(room);
|
||||
self.init_invite_button(room);
|
||||
self.set_state(Membership::Join);
|
||||
}
|
||||
|
||||
priv_.room.replace(room);
|
||||
self.notify("room");
|
||||
}
|
||||
|
||||
fn init_members_list(&self, room: &Room) {
|
||||
let priv_ = self.imp();
|
||||
|
||||
// Sort the members list by power level, then display name.
|
||||
let sorter = gtk::MultiSorter::new();
|
||||
|
@ -187,6 +217,7 @@ impl MemberPage {
|
|||
.sort_order(gtk::SortType::Descending)
|
||||
.build(),
|
||||
);
|
||||
|
||||
sorter.append(>k::StringSorter::new(Some(
|
||||
>k::PropertyExpression::new(
|
||||
Member::static_type(),
|
||||
|
@ -194,134 +225,29 @@ impl MemberPage {
|
|||
"display-name",
|
||||
),
|
||||
)));
|
||||
let sorted_members = gtk::SortListModel::new(Some(&joined_members), Some(&sorter));
|
||||
|
||||
fn search_string(member: Member) -> String {
|
||||
format!(
|
||||
"{} {} {} {}",
|
||||
member.display_name(),
|
||||
member.user_id(),
|
||||
member.role(),
|
||||
member.power_level(),
|
||||
)
|
||||
}
|
||||
let members = gtk::SortListModel::new(Some(room.members()), Some(&sorter));
|
||||
|
||||
let member_expr = gtk::ClosureExpression::new::<String, &[gtk::Expression], _>(
|
||||
&[],
|
||||
closure!(|member: Option<Member>| { member.map(search_string).unwrap_or_default() }),
|
||||
);
|
||||
let filter = gtk::StringFilter::builder()
|
||||
.match_mode(gtk::StringFilterMatchMode::Substring)
|
||||
.expression(&member_expr)
|
||||
.ignore_case(true)
|
||||
.build();
|
||||
priv_
|
||||
.members_search_entry
|
||||
.bind_property("text", &filter, "search")
|
||||
.flags(glib::BindingFlags::SYNC_CREATE)
|
||||
.build();
|
||||
let joined_members = self.build_filtered_list(&members, Membership::Join);
|
||||
let invited_members = self.build_filtered_list(&members, Membership::Invite);
|
||||
let banned_members = self.build_filtered_list(&members, Membership::Ban);
|
||||
|
||||
let filter_model = gtk::FilterListModel::new(Some(&sorted_members), Some(&filter));
|
||||
let model = gtk::NoSelection::new(Some(&filter_model));
|
||||
priv_.members_list_view.set_model(Some(&model));
|
||||
}
|
||||
|
||||
fn member_count_changed(&self, n: u32) {
|
||||
let priv_ = self.imp();
|
||||
priv_
|
||||
.member_count
|
||||
// Translators: Do NOT translate the content between '{' and '}', this is a variable
|
||||
// name.
|
||||
.set_text(&ngettext_f(
|
||||
"1 Member",
|
||||
"{n} Members",
|
||||
n,
|
||||
&[("n", &n.to_string())],
|
||||
));
|
||||
// FIXME: This won't be needed when we can request the natural height
|
||||
// on AdwPreferencesPage
|
||||
// See: https://gitlab.gnome.org/GNOME/libadwaita/-/issues/77
|
||||
if n > 5 {
|
||||
priv_.members_scroll.set_min_content_height(MAX_LIST_HEIGHT);
|
||||
} else {
|
||||
priv_.members_scroll.set_min_content_height(-1);
|
||||
}
|
||||
}
|
||||
|
||||
fn init_invited_list(&self) {
|
||||
let priv_ = self.imp();
|
||||
let members = self.room().members();
|
||||
|
||||
// Only keep the members that are in the join membership state
|
||||
let invited_expression = gtk::PropertyExpression::new(
|
||||
Member::static_type(),
|
||||
gtk::Expression::NONE,
|
||||
"membership",
|
||||
)
|
||||
.chain_closure::<bool>(closure!(
|
||||
|_: Option<glib::Object>, membership: Membership| { membership == Membership::Invite }
|
||||
));
|
||||
let invited_filter = gtk::BoolFilter::new(Some(invited_expression));
|
||||
let invited_members = gtk::FilterListModel::new(Some(members), Some(&invited_filter));
|
||||
|
||||
// Set up the invited section visibility and the invited count.
|
||||
self.invited_count_changed(invited_members.n_items());
|
||||
invited_members.connect_items_changed(
|
||||
clone!(@weak self as obj => move |members, _, _, _| {
|
||||
obj.invited_count_changed(members.n_items());
|
||||
}),
|
||||
let main_list = ExtraLists::new(
|
||||
&joined_members,
|
||||
&MembershipSubpageItem::new(Membership::Invite, &invited_members),
|
||||
&MembershipSubpageItem::new(Membership::Ban, &banned_members),
|
||||
);
|
||||
|
||||
// Sort the invited list by display name.
|
||||
let sorter = gtk::StringSorter::new(Some(>k::PropertyExpression::new(
|
||||
Member::static_type(),
|
||||
gtk::Expression::NONE,
|
||||
"display-name",
|
||||
)));
|
||||
let sorted_invited = gtk::SortListModel::new(Some(&invited_members), Some(&sorter));
|
||||
|
||||
let model = gtk::NoSelection::new(Some(&sorted_invited));
|
||||
priv_.invited_list_view.set_model(Some(&model));
|
||||
}
|
||||
|
||||
fn invited_count_changed(&self, n: u32) {
|
||||
let priv_ = self.imp();
|
||||
priv_.invited_section.set_visible(n > 0);
|
||||
priv_
|
||||
.invited_section
|
||||
// Translators: Do NOT translate the content between '{' and '}', this is a variable
|
||||
// name.
|
||||
.set_title(&ngettext_f(
|
||||
"1 Invited",
|
||||
"{n} Invited",
|
||||
n,
|
||||
&[("n", &n.to_string())],
|
||||
));
|
||||
// FIXME: This won't be needed when we can request the natural height
|
||||
// on AdwPreferencesPage
|
||||
// See: https://gitlab.gnome.org/GNOME/libadwaita/-/issues/77
|
||||
if n > 5 {
|
||||
priv_.invited_scroll.set_min_content_height(MAX_LIST_HEIGHT);
|
||||
} else {
|
||||
priv_.invited_scroll.set_min_content_height(-1);
|
||||
}
|
||||
}
|
||||
|
||||
fn init_invite_button(&self) {
|
||||
let invite_button = &*self.imp().invite_button;
|
||||
|
||||
let invite_possible = self.room().new_allowed_expr(RoomAction::Invite);
|
||||
const NONE_OBJECT: Option<&glib::Object> = None;
|
||||
invite_possible.bind(invite_button, "sensitive", NONE_OBJECT);
|
||||
|
||||
invite_button.connect_clicked(clone!(@weak self as obj => move |_| {
|
||||
let window = obj
|
||||
.root()
|
||||
.unwrap()
|
||||
.downcast::<RoomDetails>()
|
||||
.unwrap();
|
||||
window.present_invite_subpage();
|
||||
}));
|
||||
let mut list_stack_children = priv_.list_stack_children.borrow_mut();
|
||||
let joined_view = MembersListView::new(&main_list);
|
||||
priv_.list_stack.add_child(&joined_view);
|
||||
list_stack_children.insert(Membership::Join, joined_view.downgrade());
|
||||
let invited_view = MembersListView::new(&invited_members);
|
||||
priv_.list_stack.add_child(&invited_view);
|
||||
list_stack_children.insert(Membership::Invite, invited_view.downgrade());
|
||||
let banned_view = MembersListView::new(&banned_members);
|
||||
priv_.list_stack.add_child(&banned_view);
|
||||
list_stack_children.insert(Membership::Ban, banned_view.downgrade());
|
||||
}
|
||||
|
||||
pub fn member_menu(&self) -> &MemberMenu {
|
||||
|
@ -352,4 +278,120 @@ impl MemberPage {
|
|||
member.upcast::<User>().verify_identity().await;
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn state(&self) -> Membership {
|
||||
self.imp().state.get()
|
||||
}
|
||||
|
||||
pub fn set_state(&self, state: Membership) {
|
||||
let priv_ = self.imp();
|
||||
|
||||
if self.state() == state {
|
||||
return;
|
||||
}
|
||||
|
||||
if state == Membership::Join {
|
||||
priv_
|
||||
.list_stack
|
||||
.set_transition_type(gtk::StackTransitionType::SlideRight)
|
||||
} else {
|
||||
priv_
|
||||
.list_stack
|
||||
.set_transition_type(gtk::StackTransitionType::SlideLeft)
|
||||
}
|
||||
|
||||
if let Some(window) = self.root().and_then(|w| w.downcast::<adw::Window>().ok()) {
|
||||
match state {
|
||||
Membership::Invite => window.set_title(Some(&gettext("Invited Room Members"))),
|
||||
Membership::Ban => window.set_title(Some(&gettext("Banned Room Members"))),
|
||||
_ => window.set_title(Some(&gettext("Room Members"))),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(view) = priv_
|
||||
.list_stack_children
|
||||
.borrow()
|
||||
.get(&state)
|
||||
.and_then(glib::WeakRef::upgrade)
|
||||
{
|
||||
priv_.list_stack.set_visible_child(&view);
|
||||
}
|
||||
|
||||
self.imp().state.set(state);
|
||||
self.notify("state");
|
||||
}
|
||||
|
||||
fn build_filtered_list(
|
||||
&self,
|
||||
model: &impl IsA<gio::ListModel>,
|
||||
state: Membership,
|
||||
) -> gio::ListModel {
|
||||
let membership_expression = gtk::PropertyExpression::new(
|
||||
Member::static_type(),
|
||||
gtk::Expression::NONE,
|
||||
"membership",
|
||||
)
|
||||
.chain_closure::<bool>(closure!(
|
||||
|_: Option<glib::Object>, this_state: Membership| this_state == state
|
||||
));
|
||||
|
||||
let membership_filter = gtk::BoolFilter::new(Some(&membership_expression));
|
||||
|
||||
fn search_string(member: Member) -> String {
|
||||
format!(
|
||||
"{} {} {} {}",
|
||||
member.display_name(),
|
||||
member.user_id(),
|
||||
member.role(),
|
||||
member.power_level(),
|
||||
)
|
||||
}
|
||||
|
||||
let member_expr = gtk::ClosureExpression::new::<String, &[gtk::Expression], _>(
|
||||
&[],
|
||||
closure!(|member: Option<Member>| { member.map(search_string).unwrap_or_default() }),
|
||||
);
|
||||
let search_filter = gtk::StringFilter::builder()
|
||||
.match_mode(gtk::StringFilterMatchMode::Substring)
|
||||
.expression(&member_expr)
|
||||
.ignore_case(true)
|
||||
.build();
|
||||
|
||||
self.imp()
|
||||
.members_search_entry
|
||||
.bind_property("text", &search_filter, "search")
|
||||
.flags(glib::BindingFlags::SYNC_CREATE)
|
||||
.build();
|
||||
|
||||
let filter = gtk::EveryFilter::new();
|
||||
|
||||
filter.append(&membership_filter);
|
||||
filter.append(&search_filter);
|
||||
|
||||
let filter_model = gtk::FilterListModel::new(Some(model), Some(&filter));
|
||||
filter_model.upcast()
|
||||
}
|
||||
|
||||
fn init_invite_button(&self, room: &Room) {
|
||||
let invite_possible = room.new_allowed_expr(RoomAction::Invite);
|
||||
|
||||
let watch = invite_possible.watch(
|
||||
glib::Object::NONE,
|
||||
clone!(@weak self as obj => move || {
|
||||
obj.update_invite_button();
|
||||
}),
|
||||
);
|
||||
|
||||
self.imp().invite_action_watch.replace(Some(watch));
|
||||
self.update_invite_button();
|
||||
}
|
||||
|
||||
fn update_invite_button(&self) {
|
||||
if let Some(invite_action) = &*self.imp().invite_action_watch.borrow() {
|
||||
let allow_invite = invite_action
|
||||
.evaluate_as::<bool>()
|
||||
.expect("Created expression needs to be valid and a boolean");
|
||||
self.imp().invite_button.set_visible(allow_invite);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
mod invite_subpage;
|
||||
mod member_page;
|
||||
|
||||
use std::convert::From;
|
||||
|
||||
use adw::{prelude::*, subclass::prelude::*};
|
||||
use gettextrs::gettext;
|
||||
use gtk::{
|
||||
|
@ -17,8 +19,54 @@ use crate::{
|
|||
utils::{and_expr, or_expr},
|
||||
};
|
||||
|
||||
#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)]
|
||||
#[repr(u32)]
|
||||
#[enum_type(name = "RoomDetailsPageName")]
|
||||
pub enum PageName {
|
||||
General,
|
||||
Members,
|
||||
Invite,
|
||||
}
|
||||
|
||||
impl Default for PageName {
|
||||
fn default() -> Self {
|
||||
Self::General
|
||||
}
|
||||
}
|
||||
|
||||
impl glib::variant::StaticVariantType for PageName {
|
||||
fn static_variant_type() -> std::borrow::Cow<'static, glib::VariantTy> {
|
||||
String::static_variant_type()
|
||||
}
|
||||
}
|
||||
|
||||
impl glib::variant::FromVariant for PageName {
|
||||
fn from_variant(variant: &glib::variant::Variant) -> Option<Self> {
|
||||
match variant.str()? {
|
||||
"general" => Some(PageName::General),
|
||||
"members" => Some(PageName::Members),
|
||||
"invite" => Some(PageName::Invite),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl glib::variant::ToVariant for PageName {
|
||||
fn to_variant(&self) -> glib::variant::Variant {
|
||||
match self {
|
||||
PageName::General => "general",
|
||||
PageName::Members => "members",
|
||||
PageName::Invite => "invite",
|
||||
}
|
||||
.to_variant()
|
||||
}
|
||||
}
|
||||
|
||||
mod imp {
|
||||
use std::cell::Cell;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
collections::HashMap,
|
||||
};
|
||||
|
||||
use glib::subclass::InitializingObject;
|
||||
use once_cell::unsync::OnceCell;
|
||||
|
@ -31,6 +79,8 @@ mod imp {
|
|||
pub room: OnceCell<Room>,
|
||||
pub avatar_chooser: OnceCell<gtk::FileChooserNative>,
|
||||
#[template_child]
|
||||
pub main_stack: TemplateChild<gtk::Stack>,
|
||||
#[template_child]
|
||||
pub avatar_remove_button: TemplateChild<adw::Bin>,
|
||||
#[template_child]
|
||||
pub avatar_edit_button: TemplateChild<adw::Bin>,
|
||||
|
@ -44,15 +94,19 @@ mod imp {
|
|||
pub room_topic_entry: TemplateChild<CustomEntry>,
|
||||
#[template_child]
|
||||
pub room_topic_label: TemplateChild<gtk::Label>,
|
||||
pub member_page: OnceCell<MemberPage>,
|
||||
#[template_child]
|
||||
pub members_count: TemplateChild<gtk::Label>,
|
||||
pub edit_mode: Cell<bool>,
|
||||
pub list_stack_children: RefCell<HashMap<PageName, glib::WeakRef<gtk::Widget>>>,
|
||||
pub visible_page: Cell<PageName>,
|
||||
pub previous_visible_page: RefCell<Vec<PageName>>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for RoomDetails {
|
||||
const NAME: &'static str = "RoomDetails";
|
||||
type Type = super::RoomDetails;
|
||||
type ParentType = adw::PreferencesWindow;
|
||||
type ParentType = adw::Window;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
CustomEntry::static_type();
|
||||
|
@ -63,6 +117,16 @@ mod imp {
|
|||
klass.install_action("details.remove-avatar", None, move |widget, _, _| {
|
||||
widget.room().store_avatar(None)
|
||||
});
|
||||
klass.install_action("details.next-page", Some("s"), move |widget, _, param| {
|
||||
let page = param
|
||||
.and_then(|variant| variant.get::<PageName>())
|
||||
.expect("The parameter need to be a valid PageName");
|
||||
|
||||
widget.next_page(page);
|
||||
});
|
||||
klass.install_action("details.previous-page", None, move |widget, _, _| {
|
||||
widget.previous_page();
|
||||
});
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
|
@ -74,13 +138,23 @@ mod imp {
|
|||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
use once_cell::sync::Lazy;
|
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||
vec![glib::ParamSpecObject::new(
|
||||
"room",
|
||||
"Room",
|
||||
"The room backing all details of the preference window",
|
||||
Room::static_type(),
|
||||
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
|
||||
)]
|
||||
vec![
|
||||
glib::ParamSpecObject::new(
|
||||
"room",
|
||||
"Room",
|
||||
"The room backing all details of the preference window",
|
||||
Room::static_type(),
|
||||
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
|
||||
),
|
||||
glib::ParamSpecEnum::new(
|
||||
"visible-page",
|
||||
"Visible Page",
|
||||
"The page currently visible",
|
||||
PageName::static_type(),
|
||||
PageName::default() as i32,
|
||||
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
|
||||
),
|
||||
]
|
||||
});
|
||||
|
||||
PROPERTIES.as_ref()
|
||||
|
@ -95,13 +169,15 @@ mod imp {
|
|||
) {
|
||||
match pspec.name() {
|
||||
"room" => obj.set_room(value.get().unwrap()),
|
||||
"visible-page" => obj.set_visible_page(value.get().unwrap()),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn property(&self, _obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
match pspec.name() {
|
||||
"room" => self.room.get().to_value(),
|
||||
"visible-page" => obj.visible_page().to_value(),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
@ -109,26 +185,34 @@ mod imp {
|
|||
fn constructed(&self, obj: &Self::Type) {
|
||||
self.parent_constructed(obj);
|
||||
|
||||
let member_page = MemberPage::new(obj.room());
|
||||
obj.add(&member_page);
|
||||
self.member_page.set(member_page).unwrap();
|
||||
|
||||
obj.init_avatar();
|
||||
obj.init_edit_toggle();
|
||||
obj.init_avatar_chooser();
|
||||
obj.init_member_action();
|
||||
|
||||
self.main_stack
|
||||
.connect_visible_child_notify(clone!(@weak obj => move |_| {
|
||||
obj.notify("visible-page");
|
||||
}));
|
||||
|
||||
let members = obj.room().members();
|
||||
members.connect_items_changed(clone!(@weak obj => move |members, _, _, _| {
|
||||
obj.member_count_changed(members.n_items());
|
||||
}));
|
||||
|
||||
obj.member_count_changed(members.n_items());
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for RoomDetails {}
|
||||
impl WindowImpl for RoomDetails {}
|
||||
impl AdwWindowImpl for RoomDetails {}
|
||||
impl PreferencesWindowImpl for RoomDetails {}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
/// Preference Window to display and update room details.
|
||||
pub struct RoomDetails(ObjectSubclass<imp::RoomDetails>)
|
||||
@extends gtk::Widget, gtk::Window, adw::Window, gtk::Root, adw::PreferencesWindow, @implements gtk::Accessible;
|
||||
@extends gtk::Widget, gtk::Window, adw::Window, gtk::Root, @implements gtk::Accessible;
|
||||
}
|
||||
|
||||
impl RoomDetails {
|
||||
|
@ -146,6 +230,63 @@ impl RoomDetails {
|
|||
self.imp().room.set(room).expect("Room already initialized");
|
||||
}
|
||||
|
||||
pub fn visible_page(&self) -> PageName {
|
||||
self.imp().visible_page.get()
|
||||
}
|
||||
|
||||
pub fn set_visible_page(&self, name: PageName) {
|
||||
let priv_ = self.imp();
|
||||
let prev_name = self.visible_page();
|
||||
let mut list_stack_children = priv_.list_stack_children.borrow_mut();
|
||||
|
||||
if prev_name == name {
|
||||
return;
|
||||
}
|
||||
|
||||
match name {
|
||||
PageName::General => {
|
||||
self.set_title(Some(&gettext("Room Details")));
|
||||
priv_.main_stack.set_visible_child_name("general");
|
||||
}
|
||||
PageName::Members => {
|
||||
let members_page = if let Some(members_page) = list_stack_children
|
||||
.get(&PageName::Members)
|
||||
.and_then(glib::object::WeakRef::upgrade)
|
||||
{
|
||||
members_page
|
||||
} else {
|
||||
let members_page = MemberPage::new(self.room()).upcast::<gtk::Widget>();
|
||||
list_stack_children.insert(PageName::Members, members_page.downgrade());
|
||||
self.imp().main_stack.add_child(&members_page);
|
||||
members_page
|
||||
};
|
||||
|
||||
self.set_title(Some(&gettext("Room Members")));
|
||||
priv_.main_stack.set_visible_child(&members_page);
|
||||
}
|
||||
PageName::Invite => {
|
||||
priv_.main_stack.set_visible_child_name("general");
|
||||
let invite_page = if let Some(invite_page) = list_stack_children
|
||||
.get(&PageName::Invite)
|
||||
.and_then(glib::object::WeakRef::upgrade)
|
||||
{
|
||||
invite_page
|
||||
} else {
|
||||
let invite_page = InviteSubpage::new(self.room()).upcast::<gtk::Widget>();
|
||||
list_stack_children.insert(PageName::Invite, invite_page.downgrade());
|
||||
priv_.main_stack.add_child(&invite_page);
|
||||
invite_page
|
||||
};
|
||||
|
||||
self.set_title(Some(&gettext("Invite new Members")));
|
||||
priv_.main_stack.set_visible_child(&invite_page);
|
||||
}
|
||||
}
|
||||
|
||||
priv_.visible_page.set(name);
|
||||
self.notify("visible-page");
|
||||
}
|
||||
|
||||
fn init_avatar(&self) {
|
||||
let priv_ = self.imp();
|
||||
let avatar_remove_button = &priv_.avatar_remove_button;
|
||||
|
@ -254,18 +395,39 @@ impl RoomDetails {
|
|||
self.avatar_chooser().show();
|
||||
}
|
||||
|
||||
pub fn present_invite_subpage(&self) {
|
||||
self.set_title(Some(&gettext("Invite new Members")));
|
||||
let subpage = InviteSubpage::new(self.room());
|
||||
self.present_subpage(&subpage);
|
||||
fn member_count_changed(&self, n: u32) {
|
||||
self.imp().members_count.set_text(&format!("{}", n));
|
||||
}
|
||||
|
||||
pub fn close_invite_subpage(&self) {
|
||||
self.set_title(Some(&gettext("Room Details")));
|
||||
self.close_subpage();
|
||||
fn next_page(&self, next_page: PageName) {
|
||||
let priv_ = self.imp();
|
||||
let prev_page = self.visible_page();
|
||||
|
||||
if prev_page == next_page {
|
||||
return;
|
||||
}
|
||||
|
||||
priv_
|
||||
.main_stack
|
||||
.set_transition_type(gtk::StackTransitionType::SlideLeft);
|
||||
|
||||
priv_.previous_visible_page.borrow_mut().push(prev_page);
|
||||
self.set_visible_page(next_page);
|
||||
}
|
||||
|
||||
pub fn member_page(&self) -> &MemberPage {
|
||||
self.imp().member_page.get().unwrap()
|
||||
fn previous_page(&self) {
|
||||
let priv_ = self.imp();
|
||||
|
||||
priv_
|
||||
.main_stack
|
||||
.set_transition_type(gtk::StackTransitionType::SlideRight);
|
||||
|
||||
if let Some(prev_page) = priv_.previous_visible_page.borrow_mut().pop() {
|
||||
self.set_visible_page(prev_page);
|
||||
} else {
|
||||
// If there isn't any previous page close the dialog since it was opened on a
|
||||
// specific page
|
||||
self.close();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ use crate::{
|
|||
components::{CustomEntry, DragOverlay, Pill, ReactionChooser, RoomTitle},
|
||||
i18n::gettext_f,
|
||||
session::{
|
||||
content::{MarkdownPopover, RoomDetails},
|
||||
content::{room_details, MarkdownPopover, RoomDetails},
|
||||
room::{Room, RoomType, SupportedEvent, Timeline, TimelineItem, TimelineState},
|
||||
user::UserExt,
|
||||
},
|
||||
|
@ -135,10 +135,10 @@ mod imp {
|
|||
});
|
||||
|
||||
klass.install_action("room-history.details", None, move |widget, _, _| {
|
||||
widget.open_room_details("general");
|
||||
widget.open_room_details(room_details::PageName::General);
|
||||
});
|
||||
klass.install_action("room-history.invite-members", None, move |widget, _, _| {
|
||||
widget.open_invite_members();
|
||||
widget.open_room_details(room_details::PageName::Invite);
|
||||
});
|
||||
|
||||
klass.install_action("room-history.scroll-down", None, move |widget, _, _| {
|
||||
|
@ -574,19 +574,10 @@ impl RoomHistory {
|
|||
}
|
||||
|
||||
/// Opens the room details on the page with the given name.
|
||||
pub fn open_room_details(&self, page_name: &str) {
|
||||
pub fn open_room_details(&self, page_name: room_details::PageName) {
|
||||
if let Some(room) = self.room() {
|
||||
let window = RoomDetails::new(&self.parent_window(), &room);
|
||||
window.set_property("visible-page-name", page_name);
|
||||
window.show();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_invite_members(&self) {
|
||||
if let Some(room) = self.room() {
|
||||
let window = RoomDetails::new(&self.parent_window(), &room);
|
||||
window.set_property("visible-page-name", "members");
|
||||
window.present_invite_subpage();
|
||||
window.set_visible_page(page_name);
|
||||
window.show();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use gettextrs::gettext;
|
||||
use gtk::{glib, prelude::*, subclass::prelude::*};
|
||||
use matrix_sdk::{
|
||||
room::RoomMember,
|
||||
|
@ -9,6 +10,7 @@ use matrix_sdk::{
|
|||
OwnedMxcUri, UserId,
|
||||
},
|
||||
};
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
|
||||
use crate::{
|
||||
prelude::*,
|
||||
|
@ -21,7 +23,7 @@ use crate::{
|
|||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)]
|
||||
#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, glib::Enum, IntoPrimitive, TryFromPrimitive)]
|
||||
#[repr(u32)]
|
||||
#[enum_type(name = "Membership")]
|
||||
pub enum Membership {
|
||||
|
@ -39,6 +41,20 @@ impl Default for Membership {
|
|||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Membership {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let label = match self {
|
||||
Membership::Leave => gettext("Left"),
|
||||
Membership::Join => gettext("Joined"),
|
||||
Membership::Invite => gettext("Invited"),
|
||||
Membership::Ban => gettext("Banned"),
|
||||
Membership::Knock => gettext("Knocked"),
|
||||
Membership::Custom => gettext("Custom"),
|
||||
};
|
||||
f.write_str(&label)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&MembershipState> for Membership {
|
||||
fn from(state: &MembershipState) -> Self {
|
||||
match state {
|
||||
|
|
Loading…
Reference in a new issue