fix(@desktop/chat): rework invitation bubbles

InvitationBubbleView.qml:
- rework view to use layouts properly
- add missing community identicon

Backend:
- remove local community requests on community join
- propagate SIGNAL_COMMUNITY_MY_REQUEST_ADDED to UI

fixes: #7139
This commit is contained in:
Patryk Osmaczko 2022-09-13 09:56:47 +02:00 committed by osmaczko
parent 6b25ea67a8
commit d1b3e7af9e
8 changed files with 269 additions and 217 deletions

View File

@ -62,6 +62,10 @@ proc init*(self: Controller) =
let args = CommunityMutedArgs(e) let args = CommunityMutedArgs(e)
self.delegate.communityMuted(args.communityId, args.muted) self.delegate.communityMuted(args.communityId, args.muted)
self.events.on(SIGNAL_COMMUNITY_MY_REQUEST_ADDED) do(e:Args):
let args = CommunityRequestArgs(e)
self.delegate.communityAccessRequested(args.communityRequest.communityId)
self.events.on(SIGNAL_DISCORD_CATEGORIES_AND_CHANNELS_EXTRACTED) do(e:Args): self.events.on(SIGNAL_DISCORD_CATEGORIES_AND_CHANNELS_EXTRACTED) do(e:Args):
let args = DiscordCategoriesAndChannelsArgs(e) let args = DiscordCategoriesAndChannelsArgs(e)
self.delegate.discordCategoriesAndChannelsExtracted(args.categories, args.channels, args.oldestMessageTimestamp, args.errors, args.errorsCount) self.delegate.discordCategoriesAndChannelsExtracted(args.categories, args.channels, args.oldestMessageTimestamp, args.errors, args.errorsCount)

View File

@ -109,6 +109,9 @@ method viewDidLoad*(self: AccessInterface) {.base.} =
method communityMuted*(self: AccessInterface, communityId: string, muted: bool) {.base.} = method communityMuted*(self: AccessInterface, communityId: string, muted: bool) {.base.} =
raise newException(ValueError, "No implementation available") raise newException(ValueError, "No implementation available")
method communityAccessRequested*(self: AccessInterface, communityId: string) {.base.} =
raise newException(ValueError, "No implementation available")
method requestExtractDiscordChannelsAndCategories*(self: AccessInterface, filesToImport: seq[string]) {.base.} = method requestExtractDiscordChannelsAndCategories*(self: AccessInterface, filesToImport: seq[string]) {.base.} =
raise newException(ValueError, "No implementation available") raise newException(ValueError, "No implementation available")

View File

@ -232,6 +232,9 @@ method reorderCommunityCategories*(self: Module, communityId: string, categoryId
method communityMuted*(self: Module, communityId: string, muted: bool) = method communityMuted*(self: Module, communityId: string, muted: bool) =
self.view.model().setMuted(communityId, muted) self.view.model().setMuted(communityId, muted)
method communityAccessRequested*(self: Module, communityId: string) =
self.view.communityAccessRequested(communityId)
method discordCategoriesAndChannelsExtracted*(self: Module, categories: seq[DiscordCategoryDto], channels: seq[DiscordChannelDto], oldestMessageTimestamp: int, errors: Table[string, DiscordImportError], errorsCount: int) = method discordCategoriesAndChannelsExtracted*(self: Module, categories: seq[DiscordCategoryDto], channels: seq[DiscordChannelDto], oldestMessageTimestamp: int, errors: Table[string, DiscordImportError], errorsCount: int) =
for filePath in errors.keys: for filePath in errors.keys:

View File

@ -76,6 +76,7 @@ QtObject:
proc communityChanged*(self: View, communityId: string) {.signal.} proc communityChanged*(self: View, communityId: string) {.signal.}
proc discordOldestMessageTimestampChanged*(self: View) {.signal.} proc discordOldestMessageTimestampChanged*(self: View) {.signal.}
proc discordImportErrorsCountChanged*(self: View) {.signal.} proc discordImportErrorsCountChanged*(self: View) {.signal.}
proc communityAccessRequested*(self: View, communityId: string) {.signal.}
proc setCommunityTags*(self: View, communityTags: string) = proc setCommunityTags*(self: View, communityTags: string) =
self.communityTags = newQVariant(communityTags) self.communityTags = newQVariant(communityTags)

View File

@ -32,7 +32,7 @@ type CommunityMembershipRequestDto* = object
chatId*: string chatId*: string
communityId*: string communityId*: string
state*: int state*: int
our*: string our*: string #FIXME: should be bool
type CommunitySettingsDto* = object type CommunitySettingsDto* = object
id*: string id*: string

View File

@ -1,4 +1,4 @@
import NimQml, Tables, json, sequtils, std/algorithm, strformat, strutils, chronicles, json_serialization import NimQml, Tables, json, sequtils, std/algorithm, strformat, strutils, chronicles, json_serialization, sugar
import ./dto/community as community_dto import ./dto/community as community_dto
@ -265,6 +265,7 @@ QtObject:
if(not self.joinedCommunities.hasKey(community.id)): if(not self.joinedCommunities.hasKey(community.id)):
if (community.joined and community.isMember): if (community.joined and community.isMember):
self.joinedCommunities[community.id] = community self.joinedCommunities[community.id] = community
keepIf(self.myCommunityRequests, request => request.communityId != community.id)
self.events.emit(SIGNAL_COMMUNITY_JOINED, CommunityArgs(community: community, fromUserAction: false)) self.events.emit(SIGNAL_COMMUNITY_JOINED, CommunityArgs(community: community, fromUserAction: false))
return return

View File

@ -7,48 +7,60 @@ import utils 1.0
import StatusQ.Core 0.1 import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1 import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import shared.panels 1.0 import shared.panels 1.0
import shared.popups 1.0 import shared.popups 1.0
Item { Item {
id: root id: root
anchors.left: parent.left
height: rectangleBubbleLoader.height
width: rectangleBubbleLoader.width
property string communityId implicitHeight: loader.height
property var invitedCommunity implicitWidth: loader.width
property int innerMargin: 12
property bool isLink: false
property var store property var store
property string communityId
function getCommunity() { QtObject {
try { id: d
const communityJson = root.store.getSectionByIdJson(communityId)
if (!communityJson) { property var invitedCommunity
root.store.requestCommunityInfo(communityId) property bool invitationPending
return null
readonly property int margin: 12
function getCommunity() {
try {
const communityJson = root.store.getSectionByIdJson(communityId)
if (!communityJson) {
root.store.requestCommunityInfo(communityId)
return null
}
return JSON.parse(communityJson);
} catch (e) {
console.error("Error parsing community", e)
} }
return JSON.parse(communityJson); return null
} catch (e) {
console.error("Error parsing community", e)
} }
return null function reevaluate() {
invitedCommunity = getCommunity()
invitationPending = root.store.isCommunityRequestPending(communityId)
}
} }
Component.onCompleted: { Component.onCompleted: {
root.invitedCommunity = getCommunity() d.reevaluate()
} }
Connections { Connections {
target: root.store.communitiesModuleInst target: root.store.communitiesModuleInst
onCommunityChanged: function (communityId) { onCommunityChanged: function (communityId) {
if (communityId === root.communityId) { if (communityId === root.communityId) {
root.invitedCommunity = getCommunity() d.reevaluate()
} }
} }
} }
@ -57,135 +69,128 @@ Item {
target: root.store.communitiesModuleInst target: root.store.communitiesModuleInst
onCommunityAdded: function (communityId) { onCommunityAdded: function (communityId) {
if (communityId === root.communityId) { if (communityId === root.communityId) {
root.invitedCommunity = getCommunity() d.reevaluate()
} }
} }
} }
Component { Connections {
id: communityIntroDialog target: root.store.communitiesModuleInst
onCommunityAccessRequested: function (communityId) {
CommunityIntroDialog { if (communityId === root.communityId) {
anchors.centerIn: parent d.reevaluate()
}
property var joinMethod: () => {}
name: root.invitedCommunity ? root.invitedCommunity.name : ""
introMessage: root.invitedCommunity ? root.invitedCommunity.introMessage : ""
imageSrc: root.invitedCommunity ? root.invitedCommunity.image : ""
onJoined: joinMethod()
} }
} }
Loader { Loader {
id: rectangleBubbleLoader id: loader
active: !!invitedCommunity
sourceComponent: Component { active: !!d.invitedCommunity
Rectangle {
id: rectangleBubble
property alias button: joinBtn
property bool isPendingRequest: root.store.isCommunityRequestPending(communityId)
width: 270
height: column.implicitHeight
radius: 16
color: Style.current.background
border.color: Style.current.border
border.width: 1
states: [ sourceComponent: Rectangle {
State { id: rectangleBubble
name: "requiresEns"
when: invitedCommunity.ensOnly && !userProfile.ensName
PropertyChanges {
target: joinBtn
text: qsTr("Membership requires an ENS username")
enabled: false
}
},
State {
name: "inviteOnly"
when: invitedCommunity.access === Constants.communityChatInvitationOnlyAccess
PropertyChanges {
target: joinBtn
text: qsTr("You need to be invited")
enabled: false
}
},
State {
name: "pending"
when: invitedCommunity.access === Constants.communityChatOnRequestAccess &&
rectangleBubble.isPendingRequest
PropertyChanges {
target: joinBtn
text: qsTr("Pending")
enabled: false
}
},
State {
name: "joined"
when: (invitedCommunity.joined && invitedCommunity.isMember) ||
(invitedCommunity.access === Constants.communityChatPublicAccess &&
invitedCommunity.joined)
PropertyChanges {
target: joinBtn
text: qsTr("View")
}
},
State {
name: "requestToJoin"
when: invitedCommunity.access === Constants.communityChatOnRequestAccess &&
!invitedCommunity.joined && !invitedCommunity.isMember &&
invitedCommunity.canRequestAccess
PropertyChanges {
target: joinBtn
text: qsTr("Request Access")
} width: 270
}, height: columnLayout.implicitHeight
State { radius: 16
name: "unjoined" color: Style.current.background
when: (invitedCommunity.access === Constants.communityChatOnRequestAccess && border.color: Style.current.border
invitedCommunity.isMember) || border.width: 1
(invitedCommunity.access === Constants.communityChatPublicAccess &&
!invitedCommunity.joined) states: [
PropertyChanges { State {
target: joinBtn name: "requiresEns"
text: qsTr("Join") when: d.invitedCommunity.ensOnly && !userProfile.ensName
} PropertyChanges {
target: joinBtn
text: qsTr("Membership requires an ENS username")
enabled: false
} }
] },
State {
name: "inviteOnly"
when: d.invitedCommunity.access === Constants.communityChatInvitationOnlyAccess
PropertyChanges {
target: joinBtn
text: qsTr("You need to be invited")
enabled: false
}
},
State {
name: "pending"
when: d.invitedCommunity.access === Constants.communityChatOnRequestAccess &&
d.invitationPending
PropertyChanges {
target: joinBtn
text: qsTr("Pending")
enabled: false
}
},
State {
name: "joined"
when: (d.invitedCommunity.joined && d.invitedCommunity.isMember) ||
(d.invitedCommunity.access === Constants.communityChatPublicAccess &&
d.invitedCommunity.joined)
PropertyChanges {
target: joinBtn
text: qsTr("View")
}
},
State {
name: "requestToJoin"
when: d.invitedCommunity.access === Constants.communityChatOnRequestAccess &&
!d.invitedCommunity.joined
PropertyChanges {
target: joinBtn
text: qsTr("Request Access")
// Not Refactored Yet }
// Connections { },
// target: root.store.chatsModelInst.communities State {
// onMembershipRequestChanged: function(communityId, communityName, requestAccepted) { name: "unjoined"
// if (communityId === root.communityId) { when: d.invitedCommunity.access === Constants.communityChatPublicAccess &&
// rectangleBubble.isPendingRequest = false !d.invitedCommunity.joined
// } PropertyChanges {
// } target: joinBtn
// } text: qsTr("Join")
}
}
]
ColumnLayout {
id: columnLayout
width: parent.width
spacing: 0
ColumnLayout { ColumnLayout {
id: column id: invitationDescriptionLayout
width: parent.width
spacing: Style.current.halfPadding Layout.leftMargin: d.margin
Layout.rightMargin: d.margin
Layout.topMargin: 8
Layout.bottomMargin: 8
spacing: 4
// TODO add check if verified
StatusBaseText { StatusBaseText {
id: title id: title
color: invitedCommunity.verifed ? Theme.palette.primaryColor1 : Theme.palette.baseColor1
text: invitedCommunity.verifed ? Layout.fillWidth: true
qsTr("Verified community invitation") :
qsTr("Community invitation") text: d.invitedCommunity.verifed ? qsTr("Verified community invitation") : qsTr("Community invitation")
color: d.invitedCommunity.verifed ? Theme.palette.primaryColor1 : Theme.palette.baseColor1
font.weight: Font.Medium font.weight: Font.Medium
Layout.topMargin: Style.current.halfPadding
Layout.leftMargin: root.innerMargin
font.pixelSize: 13 font.pixelSize: 13
} }
StatusBaseText { StatusBaseText {
id: invitedYou id: invitedYou
Layout.fillWidth: true
visible: text != "" visible: text != ""
text: { text: {
// Not Refactored Yet // Not Refactored Yet
@ -200,109 +205,145 @@ Item {
// : qsTr("A community has been shared") // : qsTr("A community has been shared")
// } // }
} }
Layout.leftMargin: root.innerMargin
Layout.rightMargin: root.innerMargin
Layout.fillWidth: true
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
font.pixelSize: 15 font.pixelSize: 15
color: Theme.palette.directColor1
} }
}
Separator { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
} implicitHeight: 1
color: Style.current.separator
}
// TODO add image when it's supported RowLayout {
StatusBaseText { id: communityDescriptionLayout
id: communityName
text: invitedCommunity.name
Layout.topMargin: 2
Layout.leftMargin: root.innerMargin
Layout.fillWidth: true
Layout.rightMargin: root.innerMargin
font.weight: Font.Bold
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
font.pixelSize: 17
color: Theme.palette.directColor1
}
StatusBaseText { Layout.leftMargin: d.margin
id: communityDesc Layout.rightMargin: d.margin
text: invitedCommunity.description Layout.topMargin: 12
Layout.leftMargin: root.innerMargin Layout.bottomMargin: 12
Layout.rightMargin: root.innerMargin
Layout.fillWidth: true
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
font.pixelSize: 15
color: Theme.palette.directColor1
}
StatusBaseText { spacing: 12
id: communityNbMembers
// TODO add the plural support
text: qsTr("%1 members").arg(invitedCommunity.nbMembers)
Layout.leftMargin: root.innerMargin
font.pixelSize: 13
font.weight: Font.Medium
color: Theme.palette.baseColor1
}
Separator { StatusSmartIdenticon {
Layout.fillWidth: true Layout.alignment: Qt.AlignTop
} Layout.preferredWidth: 40
Layout.preferredHeight: 40
Item { name: d.invitedCommunity.name
id: btnItemId
Layout.topMargin: -column.spacing
Layout.fillWidth: true
height: 44
clip: true
StatusFlatButton {
id: joinBtn
anchors.fill: parent
anchors.verticalCenter: parent.verticalCenter
enabled: true
text: qsTr("Unsupported state")
onClicked: {
let error
if (rectangleBubble.state === "joined") { asset {
root.store.setActiveCommunity(communityId); width: 40
return height: 40
} name: d.invitedCommunity.image
if (rectangleBubble.state === "unjoined") { color: d.invitedCommunity.color
Global.openPopup(communityIntroDialog, { joinMethod: () => { isImage: true
let error = root.store.joinCommunity(communityId, userProfile.name)
if (error) joiningError.showError(error)
} });
}
else if (rectangleBubble.state === "requestToJoin") {
Global.openPopup(communityIntroDialog, { joinMethod: () => {
let error = root.store.requestToJoinCommunity(communityId, userProfile.name)
if (error) joiningError.showError(error)
else rectangleBubble.isPendingRequest = root.store.isCommunityRequestPending(communityId)
} });
}
if (error) joiningError.showError(error)
}
MessageDialog {
id: joiningError
function showError(error) {
joiningError.text = error
joiningError.open()
}
title: qsTr("Error joining the community")
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok
}
} }
} }
ColumnLayout {
spacing: 2
StatusBaseText {
Layout.fillWidth: true
text: d.invitedCommunity.name
font.weight: Font.Bold
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
font.pixelSize: 17
color: Theme.palette.directColor1
}
StatusBaseText {
Layout.fillWidth: true
text: d.invitedCommunity.description
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
font.pixelSize: 15
color: Theme.palette.directColor1
}
StatusBaseText {
Layout.fillWidth: true
// TODO add the plural support
text: qsTr("%1 members").arg(d.invitedCommunity.nbMembers)
font.pixelSize: 13
font.weight: Font.Medium
color: Theme.palette.baseColor1
}
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: 1
color: Style.current.separator
}
StatusFlatButton {
id: joinBtn
Layout.fillWidth: true
Layout.preferredHeight: 44
text: qsTr("Unsupported state")
onClicked: {
if (rectangleBubble.state === "joined") {
root.store.setActiveCommunity(communityId);
return
}
if (rectangleBubble.state === "unjoined") {
Global.openPopup(communityIntroDialog, { joinMethod: () => {
let error = root.store.joinCommunity(communityId, userProfile.name)
if (error) joiningError.showError(error)
} });
}
else if (rectangleBubble.state === "requestToJoin") {
Global.openPopup(communityIntroDialog, { joinMethod: () => {
let error = root.store.requestToJoinCommunity(communityId, userProfile.name)
if (error) joiningError.showError(error)
} });
}
}
Component.onCompleted: {
// FIXME: extract StatusButtonBackground or expose radius property in StatusBaseButton
background.radius = 16
}
} }
} }
} }
} }
Component {
id: communityIntroDialog
CommunityIntroDialog {
anchors.centerIn: parent
property var joinMethod: () => {}
name: d.invitedCommunity ? d.invitedCommunity.name : ""
introMessage: d.invitedCommunity ? d.invitedCommunity.introMessage : ""
imageSrc: d.invitedCommunity ? d.invitedCommunity.image : ""
onJoined: joinMethod()
}
}
MessageDialog {
id: joiningError
function showError(error) {
joiningError.text = error
joiningError.open()
}
title: qsTr("Error joining the community")
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok
}
} }

View File

@ -240,7 +240,6 @@ Column {
InvitationBubbleView { InvitationBubbleView {
store: root.store store: root.store
communityId: linkData.communityId communityId: linkData.communityId
isLink: true
anchors.left: parent.left anchors.left: parent.left
} }
} }