status-desktop/ui/app/AppLayouts/Communities/views/CommunityColumnView.qml

652 lines
26 KiB
QML
Raw Normal View History

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Dialogs 1.3
import QtGraphicalEffects 1.15
import QtQuick.Layouts 1.15
2020-12-11 20:29:46 +00:00
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Popups 0.1
import utils 1.0
import shared 1.0
import shared.popups 1.0
import shared.status 1.0
import shared.controls.chat.menuItems 1.0
import shared.panels 1.0
import shared.stores 1.0
import shared.views.chat 1.0
import AppLayouts.Chat.stores 1.0 as ChatStores
import AppLayouts.Communities.popups 1.0
import AppLayouts.Communities.panels 1.0
import AppLayouts.Communities.stores 1.0 as CommunitiesStores
import AppLayouts.Wallet.stores 1.0 as WalletStores
// FIXME: Rework me to use ColumnLayout instead of anchors!!
Item {
id: root
objectName: "communityColumnView"
width: Constants.chatSectionLeftColumnWidth
height: parent.height
// Important:
// We're here in case of CommunitySection
// This module is set from `ChatLayout` (each `ChatLayout` has its own communitySectionModule)
property var communitySectionModule
property var emojiPopup
property ChatStores.RootStore store
property CommunitiesStores.CommunitiesStore communitiesStore
required property WalletStores.WalletAssetsStore walletAssetsStore
required property CurrenciesStore currencyStore
property bool hasAddedContacts: false
property var communityData
property alias createChannelPopup: createChannelPopup
2020-12-11 20:29:46 +00:00
property int requestToJoinState: Constants.RequestToJoinState.None
// Community transfer ownership related props:
required property bool isPendingOwnershipRequest
signal finaliseOwnershipClicked
readonly property bool isSectionAdmin:
communityData.memberRole === Constants.memberRole.owner ||
communityData.memberRole === Constants.memberRole.admin ||
communityData.memberRole === Constants.memberRole.tokenMaster
signal infoButtonClicked
signal manageButtonClicked
QtObject {
id: d
readonly property bool showJoinButton: !communityData.joined || root.communityData.amIBanned
readonly property bool showFinaliseOwnershipButton: root.isPendingOwnershipRequest
readonly property bool discordImportInProgress: (root.communitiesStore.discordImportProgress > 0 && root.communitiesStore.discordImportProgress < 100)
|| root.communitiesStore.discordImportInProgress
readonly property int requestToJoinState: root.communitySectionModule.requestToJoinState
readonly property bool invitationPending: d.requestToJoinState !== Constants.RequestToJoinState.None
}
ColumnHeaderPanel {
2020-12-11 20:29:46 +00:00
id: communityHeader
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
name: communityData.name
membersCount: communityData.members.count
image: communityData.image
color: communityData.color
amISectionAdmin: root.isSectionAdmin
openCreateChat: root.store.openCreateChat
onInfoButtonClicked: root.infoButtonClicked()
onAdHocChatButtonClicked: root.store.openCloseCreateChatView()
}
Loader {
id: columnHeaderButton
anchors.top: communityHeader.bottom
anchors.topMargin: Theme.halfPadding
anchors.bottomMargin: Theme.halfPadding
anchors.horizontalCenter: parent.horizontalCenter
sourceComponent: d.showFinaliseOwnershipButton ? finaliseCommunityOwnershipBtn :
d.showJoinButton ? joinCommunityButton : undefined
active: d.showFinaliseOwnershipButton || d.showJoinButton
}
ChatsLoadingPanel {
chatSectionModule: root.communitySectionModule
width: parent.width
anchors.top: columnHeaderButton.active ? columnHeaderButton.bottom : communityHeader.bottom
anchors.topMargin: active ? Theme.halfPadding : 0
}
2022-12-01 16:58:37 +00:00
StatusMenu {
id: adminPopupMenu
enabled: root.isSectionAdmin
hideDisabledItems: !showInviteButton
property bool showInviteButton: false
onClosed: adminPopupMenu.showInviteButton = false
2022-12-01 16:58:37 +00:00
StatusAction {
objectName: "createCommunityChannelBtn"
text: qsTr("Create channel")
icon.name: "channel"
onTriggered: Global.openPopup(createChannelPopup)
}
StatusAction {
objectName: "importCommunityChannelBtn"
text: qsTr("Create channel via Discord import")
icon.name: "download"
enabled: !d.discordImportInProgress
onTriggered: {
Global.openPopup(createChannelPopup, {isDiscordImport: true, communityId: communityData.id})
}
}
2022-12-01 16:58:37 +00:00
StatusAction {
objectName: "createCommunityCategoryBtn"
text: qsTr("Create category")
icon.name: "channel-category"
onTriggered: Global.openPopup(createCategoryPopup)
}
StatusMenuSeparator {
visible: invitePeopleBtn.enabled
}
2022-12-01 16:58:37 +00:00
StatusAction {
id: invitePeopleBtn
text: qsTr("Invite people")
icon.name: "share-ios"
enabled: communityData.canManageUsers && adminPopupMenu.showInviteButton
objectName: "invitePeople"
onTriggered: {
Global.openInviteFriendsToCommunityPopup(root.communityData,
root.communitySectionModule,
null)
}
}
}
StatusScrollView {
id: scrollView
anchors.top: columnHeaderButton.active ? columnHeaderButton.bottom : communityHeader.bottom
anchors.topMargin: Theme.halfPadding
anchors.bottom: createChatOrCommunity.top
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width
2020-12-11 20:29:46 +00:00
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
contentWidth: availableWidth
contentHeight: communityChatListAndCategories.height
+ bannerColumn.height
+ bannerColumn.anchors.topMargin
StatusChatListAndCategories {
id: communityChatListAndCategories
width: scrollView.availableWidth
draggableItems: root.isSectionAdmin
draggableCategories: root.isSectionAdmin
model: root.communitySectionModule.model
highlightItem: !root.store.openCreateChat
onChatItemSelected: {
Global.closeCreateChatView()
root.communitySectionModule.setActiveItem(id)
}
showCategoryActionButtons: root.isSectionAdmin
showPopupMenu: root.isSectionAdmin && communityData.canManageUsers
onChatItemUnmuted: root.communitySectionModule.unmuteChat(id)
onChatItemReordered: function(categoryId, chatId, to) {
root.store.reorderCommunityChat(categoryId, chatId, to);
2022-02-01 22:41:45 +00:00
}
2022-01-25 14:51:38 +00:00
onChatListCategoryReordered: root.store.reorderCommunityCategories(categoryId, to)
onCategoryAddButtonClicked: Global.openPopup(createChannelPopup, {
categoryId: id
})
onToggleCollapsedCommunityCategory: root.store.toggleCollapsedCommunityCategory(categoryId, collapsed)
2022-12-01 16:58:37 +00:00
popupMenu: StatusMenu {
hideDisabledItems: false
2022-12-01 16:58:37 +00:00
StatusAction {
text: qsTr("Create channel")
icon.name: "channel"
enabled: root.isSectionAdmin
onTriggered: Global.openPopup(createChannelPopup)
}
StatusAction {
objectName: "importCommunityChannelBtn"
text: qsTr("Create channel via Discord import")
icon.name: "download"
enabled: !d.discordImportInProgress
onTriggered: Global.openPopup(createChannelPopup, {isDiscordImport: true, communityId: root.communityData.id})
}
2022-12-01 16:58:37 +00:00
StatusAction {
text: qsTr("Create category")
icon.name: "channel-category"
enabled: root.isSectionAdmin
2022-01-28 00:02:06 +00:00
onTriggered: Global.openPopup(createCategoryPopup)
}
2020-12-11 20:29:46 +00:00
StatusMenuSeparator {}
2022-12-01 16:58:37 +00:00
StatusAction {
text: qsTr("Invite people")
icon.name: "share-ios"
enabled: communityData.canManageUsers
objectName: "invitePeople"
onTriggered: {
Global.openInviteFriendsToCommunityPopup(root.communityData,
root.communitySectionModule,
null)
}
}
}
2022-12-01 16:58:37 +00:00
categoryPopupMenu: StatusMenu {
id: contextMenuCategory
property var categoryItem
MuteChatMenuItem {
enabled: !!categoryItem && !categoryItem.muted
title: qsTr("Mute category")
onMuteTriggered: {
root.communitySectionModule.muteCategory(categoryItem.itemId, interval)
contextMenuCategory.close()
}
}
2022-12-01 16:58:37 +00:00
StatusAction {
enabled: !!categoryItem && categoryItem.muted
text: qsTr("Unmute category")
icon.name: "notification"
onTriggered: {
root.communitySectionModule.unmuteCategory(categoryItem.itemId)
}
}
2022-12-01 16:58:37 +00:00
StatusAction {
objectName: "editCategoryMenuItem"
enabled: root.isSectionAdmin
text: qsTr("Edit Category")
icon.name: "edit"
onTriggered: {
Global.openPopup(createCategoryPopup, {
isEdit: true,
channels: [],
categoryId: categoryItem.itemId,
categoryName: categoryItem.name
})
}
}
StatusMenuSeparator {
visible: root.isSectionAdmin
}
2022-12-01 16:58:37 +00:00
StatusAction {
objectName: "deleteCategoryMenuItem"
enabled: root.isSectionAdmin
text: qsTr("Delete Category")
icon.name: "delete"
2022-12-01 16:58:37 +00:00
type: StatusAction.Type.Danger
onTriggered: {
Global.openPopup(deleteCategoryConfirmationDialogComponent, {
"headerSettings.title": qsTr("Delete '%1' category").arg(categoryItem.name),
confirmationText: qsTr("Are you sure you want to delete '%1' category? Channels inside the category won't be deleted.")
.arg(categoryItem.name),
categoryId: categoryItem.itemId
})
}
}
}
chatListPopupMenu: ChatContextMenuView {
id: chatContextMenuView
2024-04-08 11:27:19 +00:00
showDebugOptions: root.store.isDebugEnabled
// TODO pass the chatModel in its entirety instead of fetching the JSOn using just the id
openHandler: function (id) {
try {
let jsonObj = root.communitySectionModule.getItemAsJson(id)
let obj = JSON.parse(jsonObj)
if (obj.error) {
console.error("error parsing chat item json object, id: ", id, " error: ", obj.error)
close()
return
}
currentFleet = root.communitySectionModule.getCurrentFleet()
isCommunityChat = root.communitySectionModule.isCommunity()
amIChatAdmin = root.isSectionAdmin
chatId = obj.itemId
chatName = obj.name
chatDescription = obj.description
chatIcon = obj.icon
chatEmoji = obj.emoji
chatColor = obj.color
chatType = obj.type
chatMuted = obj.muted
channelPosition = obj.position
chatCategoryId = obj.categoryId
viewersCanPostReactions = obj.viewersCanPostReactions
hideIfPermissionsNotMet = obj.hideIfPermissionsNotMet
} catch (e) {
console.error("error parsing chat item json object, id: ", id, " error: ", e)
close()
return
}
}
onMuteChat: {
root.communitySectionModule.muteChat(chatId, interval)
}
onUnmuteChat: {
root.communitySectionModule.unmuteChat(chatId)
}
2021-12-08 11:10:34 +00:00
onMarkAllMessagesRead: {
root.communitySectionModule.markAllMessagesRead(chatId)
2021-12-08 11:10:34 +00:00
}
2021-12-08 13:06:31 +00:00
onRequestMoreMessages: {
root.communitySectionModule.requestMoreMessages(chatId)
}
2021-12-08 13:06:31 +00:00
onClearChatHistory: {
root.communitySectionModule.clearChatHistory(chatId)
}
onRequestAllHistoricMessages: {
// Not Refactored Yet - Check in the `master` branch if this is applicable here.
}
onLeaveChat: {
root.communitySectionModule.leaveChat(chatId)
}
onDeleteCommunityChat: root.store.removeCommunityChat(chatId)
onDownloadMessages: {
root.communitySectionModule.downloadMessages(chatId, file)
}
onDisplayProfilePopup: {
Global.openProfilePopup(publicKey)
}
onDisplayEditChannelPopup: {
Global.openPopup(createChannelPopup, {
isEdit: true,
channelName: chatName,
channelDescription: chatDescription,
channelEmoji: chatEmoji,
channelColor: chatColor,
categoryId: chatCategoryId,
chatId: chatContextMenuView.chatId,
channelPosition: channelPosition,
viewOnlyCanAddReaction: viewersCanPostReactions,
deleteChatConfirmationDialog: deleteChatConfirmationDialog,
hideIfPermissionsNotMet: hideIfPermissionsNotMet
});
}
}
2021-05-16 15:16:42 +00:00
}
Column {
id: bannerColumn
width: scrollView.availableWidth
anchors.top: communityChatListAndCategories.bottom
anchors.topMargin: Theme.padding
spacing: Theme.bigPadding
Loader {
active: root.isSectionAdmin &&
(!localAccountSensitiveSettings.hiddenCommunityWelcomeBanners ||
!localAccountSensitiveSettings.hiddenCommunityWelcomeBanners.includes(communityData.id))
width: parent.width
height: item.height
sourceComponent: Component {
WelcomeBannerPanel {
activeCommunity: communityData
store: root.store
hasAddedContacts: root.hasAddedContacts
communitySectionModule: root.communitySectionModule
onManageCommunityClicked: root.manageButtonClicked()
}
}
} // Loader
Loader {
active: root.isSectionAdmin &&
(!localAccountSensitiveSettings.hiddenCommunityChannelAndCategoriesBanners ||
!localAccountSensitiveSettings.hiddenCommunityChannelAndCategoriesBanners.includes(communityData.id))
width: parent.width
height: item.height
sourceComponent: Component {
ChannelsAndCategoriesBannerPanel {
id: channelsAndCategoriesBanner
communityId: communityData.id
onAddMembersClicked: {
Global.openPopup(createChannelPopup);
}
onAddCategoriesClicked: {
Global.openPopup(createCategoryPopup);
}
}
}
} // Loader
} // Column
background: Item {
TapHandler {
enabled: root.isSectionAdmin
acceptedButtons: Qt.RightButton
onTapped: {
adminPopupMenu.showInviteButton = true
adminPopupMenu.x = eventPoint.position.x + 4
adminPopupMenu.y = eventPoint.position.y + 4
adminPopupMenu.open()
}
}
}
} // ScrollView
Loader {
id: createChatOrCommunity
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: active ? Theme.padding : 0
active: root.isSectionAdmin
sourceComponent: Component {
StatusLinkText {
id: createChannelOrCategoryBtn
objectName: "createChannelOrCategoryBtn"
height: visible ? implicitHeight : 0
text: qsTr("Create channel or category")
font.underline: true
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
adminPopupMenu.showInviteButton = false
adminPopupMenu.popup()
adminPopupMenu.y = Qt.binding(() => root.height - adminPopupMenu.height
- createChannelOrCategoryBtn.height - 20)
}
}
}
}
}
Component {
id: joinCommunityButton
StatusButton {
anchors.top: communityHeader.bottom
anchors.topMargin: Theme.halfPadding
anchors.bottomMargin: Theme.halfPadding
anchors.horizontalCenter: parent.horizontalCenter
enabled: !root.communityData.amIBanned
loading: d.requestToJoinState === Constants.RequestToJoinState.InProgress
text: {
if (root.communityData.amIBanned) return qsTr("You were banned from community")
if (d.requestToJoinState === Constants.RequestToJoinState.Requested) return qsTr("Membership request pending...")
return root.communityData.access === Constants.communityChatOnRequestAccess ?
qsTr("Request to join") : qsTr("Join Community")
}
onClicked: {
Global.communityIntroPopupRequested(communityData.id, communityData.name, communityData.introMessage,
communityData.image, d.invitationPending)
}
Connections {
enabled: d.showJoinButton
target: root.store.communitiesModuleInst
function onCommunityAccessFailed(communityId: string, error: string) {
if (communityId === root.communityData.id) {
Global.displayToastMessage(qsTr("Request to join failed"),
qsTr("Please try again later"),
"",
false,
Constants.ephemeralNotificationType.normal,
"")
}
}
}
}
}
Component {
id: finaliseCommunityOwnershipBtn
StatusButton {
anchors.top: communityHeader.bottom
anchors.topMargin: Theme.halfPadding
anchors.bottomMargin: Theme.halfPadding
anchors.horizontalCenter: parent.horizontalCenter
text: communityData.joined ? qsTr("Finalise community ownership") : qsTr("To join, finalise community ownership")
onClicked: root.finaliseOwnershipClicked()
}
}
Component {
id: createChannelPopup
CreateChannelPopup {
communitiesStore: root.communitiesStore
assetsModel: root.store.assetsModel
collectiblesModel: root.store.collectiblesModel
ensCommunityPermissionsEnabled: root.store.ensCommunityPermissionsEnabled
fix(permissions): fix hang when all channel perm check return (#14259) * fix(permissions): fix hang when all channel perm check return Fixes #14234 The problem was that we updated **all** the models from **all** the channels of a community each time the channel requirement checks returned. The fix is to first of all, make sure we don't call that check too often. It sometimes got called twice in a row by accident. The other better fix is to check if anything actually changed before updating. This solves the issue almost entirely. Since the permissions almost never change, the updates now take only a second. * fix(permisisons): never run permission checks for privileged users Also fixes #14234 but for admins, TMs and Owners. Admins+ were still getting the hang, because the permission checks always returned something different than the models, because the models knew that admins have access to everything, but the permission check was running as if it were a normal user (I think, un-tested). Anyway, the solution is more simple, we never need to run the permission checks on admins+, because they always have access to everything! * fix(Communities): prevent channels model from emitting unnecessary signals Closes: #14274 * chore(Communities): improve channels metadata lookup performance ChannelsSelectionModel is removed, replaced with plain LeftJoinModel. Transformations of left-side model are done in a single place, not in every delegate making the join. * only call update functions when there is something to update + move permission model creation when needed --------- Co-authored-by: Michał Cieślak <michalcieslak@status.im>
2024-04-04 15:26:44 +00:00
permissionsModel: {
root.store.prepareTokenModelForCommunityChat(communityData.id, chatId)
fix(permissions): fix hang when all channel perm check return (#14259) * fix(permissions): fix hang when all channel perm check return Fixes #14234 The problem was that we updated **all** the models from **all** the channels of a community each time the channel requirement checks returned. The fix is to first of all, make sure we don't call that check too often. It sometimes got called twice in a row by accident. The other better fix is to check if anything actually changed before updating. This solves the issue almost entirely. Since the permissions almost never change, the updates now take only a second. * fix(permisisons): never run permission checks for privileged users Also fixes #14234 but for admins, TMs and Owners. Admins+ were still getting the hang, because the permission checks always returned something different than the models, because the models knew that admins have access to everything, but the permission check was running as if it were a normal user (I think, un-tested). Anyway, the solution is more simple, we never need to run the permission checks on admins+, because they always have access to everything! * fix(Communities): prevent channels model from emitting unnecessary signals Closes: #14274 * chore(Communities): improve channels metadata lookup performance ChannelsSelectionModel is removed, replaced with plain LeftJoinModel. Transformations of left-side model are done in a single place, not in every delegate making the join. * only call update functions when there is something to update + move permission model creation when needed --------- Co-authored-by: Michał Cieślak <michalcieslak@status.im>
2024-04-04 15:26:44 +00:00
return root.store.permissionsModel
}
channelsModel: root.store.chatCommunitySectionModule.model
emojiPopup: root.emojiPopup
activeCommunity: root.communityData
property int channelPosition: -1
property var deleteChatConfirmationDialog
onCreateCommunityChannel: function (chName, chDescription, chEmoji, chColor,
chCategoryId, viewOnlyCanAddReaction, hideIfPermissionsNotMet) {
root.store.createCommunityChannel(chName, chDescription, chEmoji, chColor,
chCategoryId, viewOnlyCanAddReaction, hideIfPermissionsNotMet)
chatId = root.store.currentChatContentModule().chatDetails.id
}
onEditCommunityChannel: {
root.store.editCommunityChannel(chatId,
chName,
chDescription,
chEmoji,
chColor,
chCategoryId,
channelPosition,
viewOnlyCanAddReaction,
hideIfPermissionsNotMet);
}
onAddPermissions: function (permissions) {
for (var i = 0; i < permissions.length; i++) {
root.store.permissionsStore.createPermission(permissions[i].holdingsListModel,
permissions[i].permissionType,
permissions[i].isPrivate,
permissions[i].channelsListModel)
}
}
onRemovePermissions: function (permissions) {
for (var i = 0; i < permissions.length; i++) {
root.store.permissionsStore.removePermission(permissions[i].id)
}
}
onEditPermissions: function (permissions) {
for (var i = 0; i < permissions.length; i++) {
root.store.permissionsStore.editPermission(permissions[i].id,
permissions[i].holdingsListModel,
permissions[i].permissionType,
permissions[i].channelsListModel,
permissions[i].isPrivate)
}
}
onSetHideIfPermissionsNotMet: function (checked) {
root.store.permissionsStore.setHideIfPermissionsNotMet(chatId, checked)
}
onDeleteCommunityChannel: {
Global.openPopup(deleteChatConfirmationDialog);
close()
}
onClosed: {
destroy()
}
}
}
Component {
id: createCategoryPopup
CreateCategoryPopup {
anchors.centerIn: parent
store: root.store
onClosed: {
destroy()
}
}
}
Component {
id: deleteCategoryConfirmationDialogComponent
ConfirmationDialog {
property string categoryId
confirmButtonObjectName: "confirmDeleteCategoryButton"
showCancelButton: true
onClosed: {
destroy()
}
onCancelButtonClicked: {
close();
}
onConfirmButtonClicked: function(){
const error = root.store.deleteCommunityCategory(categoryId);
if (error) {
deleteError.text = error
return deleteError.open()
}
close();
}
}
}
MessageDialog {
id: deleteError
title: qsTr("Error deleting the category")
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok
}
2020-12-11 20:29:46 +00:00
}