wip community

This commit is contained in:
Jonathan Rainville 2020-12-11 15:29:46 -05:00 committed by Iuri Matias
parent 6157744d59
commit ce3252fb8f
31 changed files with 1851 additions and 109 deletions

View File

@ -52,6 +52,10 @@ proc handleChatEvents(self: ChatController) =
self.view.setActiveChannelByIndex(0)
self.view.appReady()
self.status.events.on("communityActiveChanged") do(e:Args):
# TODO set this back to the previous one instead
self.view.setActiveChannelByIndex(0)
self.status.events.on("channelJoined") do(e: Args):
var channel = ChannelArgs(e)
if channel.chat.chatType == ChatType.Timeline:

View File

@ -14,7 +14,7 @@ import ../../status/chat/[chat, message]
import ../../status/profile/profile
import web3/[conversions, ethtypes]
import ../../status/threads
import views/[channels_list, message_list, chat_item, suggestions_list, reactions, stickers, groups, transactions]
import views/[channels_list, message_list, chat_item, suggestions_list, reactions, stickers, groups, transactions, community_list, community_item]
import json_serialization
import ../utils/image_utils
@ -35,6 +35,10 @@ QtObject:
transactions*: TransactionsView
activeChannel*: ChatItemView
previousActiveChannelIndex: int
activeCommunity*: CommunityItemView
observedCommunity*: CommunityItemView
communityList*: CommunityList
joinedCommunityList*: CommunityList
replyTo: string
channelOpenTime*: Table[string, int64]
connected: bool
@ -49,6 +53,8 @@ QtObject:
proc delete(self: ChatsView) =
self.chats.delete
self.activeChannel.delete
self.observedCommunity.delete
self.activeCommunity.delete
self.currentSuggestions.delete
for msg in self.messageList.values:
msg.delete
@ -66,11 +72,15 @@ QtObject:
result.connected = false
result.chats = newChannelsList(status)
result.activeChannel = newChatItemView(status)
result.activeCommunity = newCommunityItemView(status)
result.observedCommunity = newCommunityItemView(status)
result.currentSuggestions = newSuggestionsList()
result.messageList = initTable[string, ChatMessageList]()
result.reactions = newReactionView(status, result.messageList.addr, result.activeChannel)
result.stickers = newStickersView(status, result.activeChannel)
result.groups = newGroupsView(status,result.activeChannel)
result.communityList = newCommunityList(status)
result.joinedCommunityList = newCommunityList(status)
result.transactions = newTransactionsView(status)
result.unreadMessageCnt = 0
result.loadingMessages = false
@ -200,12 +210,19 @@ QtObject:
discard self.status.chat.markAllChannelMessagesRead(selectedChannel.id)
proc setActiveChannelByIndex*(self: ChatsView, index: int) {.slot.} =
if(self.chats.chats.len == 0): return
if((self.activeCommunity.active and self.activeCommunity.chats.chats.len == 0) or (not self.activeCommunity.active and self.chats.chats.len == 0)): return
let selectedChannel =
if (self.activeCommunity.active):
self.activeCommunity.chats.getChannel(index)
else:
self.chats.getChannel(index)
if(not self.activeChannel.chatItem.isNil and self.activeChannel.chatItem.unviewedMessagesCount > 0):
var response = self.status.chat.markAllChannelMessagesRead(self.activeChannel.id)
if not response.hasKey("error"):
self.chats.clearUnreadMessagesCount(self.activeChannel.chatItem)
let selectedChannel = self.chats.getChannel(index)
if self.activeChannel.id == selectedChannel.id: return
if selectedChannel.chatType.isOneToOne and selectedChannel.id == selectedChannel.name:
@ -215,17 +232,28 @@ QtObject:
self.activeChannel.setChatItem(selectedChannel)
self.status.chat.setActiveChannel(selectedChannel.id)
proc getActiveChannelIdx(self: ChatsView): QVariant {.slot.} =
newQVariant(self.chats.chats.findIndexById(self.activeChannel.id))
proc getActiveChannelIdx(self: ChatsView): int {.slot.} =
if (self.activeCommunity.active):
return self.activeCommunity.chats.chats.findIndexById(self.activeChannel.id)
else:
return self.chats.chats.findIndexById(self.activeChannel.id)
QtProperty[QVariant] activeChannelIndex:
QtProperty[int] activeChannelIndex:
read = getActiveChannelIdx
write = setActiveChannelByIndex
notify = activeChannelChanged
proc setActiveChannel*(self: ChatsView, channel: string) {.slot.} =
if(channel == ""): return
self.activeChannel.setChatItem(self.chats.getChannel(self.chats.chats.findIndexById(channel)))
let selectedChannel =
if (self.activeCommunity.active):
self.activeCommunity.chats.getChannel(self.activeCommunity.chats.chats.findIndexById(channel))
else:
self.chats.getChannel(self.chats.chats.findIndexById(channel))
self.activeChannel.setChatItem(selectedChannel)
discard self.status.chat.markAllChannelMessagesRead(self.activeChannel.id)
self.setLastMessageTimestamp(true)
self.activeChannelChanged()
@ -547,3 +575,120 @@ QtObject:
QtProperty[QVariant] transactions:
read = getTransactions
proc communitiesChanged*(self: ChatsView) {.signal.}
proc getCommunitiesIfNotFetched*(self: ChatsView): CommunityList =
if (not self.communityList.fetched):
let communities = self.status.chat.getAllComunities()
self.communityList.setNewData(communities)
self.communityList.fetched = true
return self.communityList
proc getComunities*(self: ChatsView): QVariant {.slot.} =
return newQVariant(self.getCommunitiesIfNotFetched())
QtProperty[QVariant] communities:
read = getComunities
notify = communitiesChanged
proc joinedCommunitiesChanged*(self: ChatsView) {.signal.}
proc getJoinedComunities*(self: ChatsView): QVariant {.slot.} =
if (not self.joinedCommunityList.fetched):
let communities = self.status.chat.getJoinedComunities()
self.joinedCommunityList.setNewData(communities)
self.joinedCommunityList.fetched = true
return newQVariant(self.joinedCommunityList)
QtProperty[QVariant] joinedCommunities:
read = getJoinedComunities
notify = joinedCommunitiesChanged
proc createCommunity*(self: ChatsView, name: string, description: string, color: string, imagePath: string): string {.slot.} =
result = ""
try:
# TODO Change this to get it from the user choices
let access = ord(CommunityAccessLevel.public)
let tmpImagePath = self.resizeImage(imagePath, 120)
let community = self.status.chat.createCommunity(name, description, color, tmpImagePath, access)
removeFile(tmpImagePath)
if (community.id == ""):
return "Community was not created. Please try again later"
self.communityList.addCommunityItemToList(community)
self.joinedCommunityList.addCommunityItemToList(community)
self.communitiesChanged()
except Exception as e:
error "Error creating the community", msg = e.msg
result = fmt"Error creating the community: {e.msg}"
proc createCommunityChannel*(self: ChatsView, communityId: string, name: string, description: string): string {.slot.} =
result = ""
try:
let chat = self.status.chat.createCommunityChannel(communityId, name, description)
if (chat.id == ""):
return "Chat was not created. Please try again later"
self.joinedCommunityList.addChannelToCommunity(communityId, chat)
discard self.activeCommunity.chats.addChatItemToList(chat)
except Exception as e:
error "Error creating the channel", msg = e.msg
result = fmt"Error creating the channel: {e.msg}"
proc activeCommunityChanged*(self: ChatsView) {.signal.}
proc setActiveCommunity*(self: ChatsView, communityId: string) {.slot.} =
if(communityId == ""): return
self.activeCommunity.setCommunityItem(self.joinedCommunityList.getCommunityById(communityId))
self.activeCommunity.setActive(true)
self.activeCommunityChanged()
proc getActiveCommunity*(self: ChatsView): QVariant {.slot.} =
newQVariant(self.activeCommunity)
QtProperty[QVariant] activeCommunity:
read = getActiveCommunity
write = setActiveCommunity
notify = activeCommunityChanged
proc observedCommunityChanged*(self: ChatsView) {.signal.}
proc setObservedCommunity*(self: ChatsView, communityId: string) {.slot.} =
if(communityId == ""): return
self.observedCommunity.setCommunityItem(self.communityList.getCommunityById(communityId))
self.observedCommunityChanged()
proc getObservedCommunity*(self: ChatsView): QVariant {.slot.} =
newQVariant(self.observedCommunity)
QtProperty[QVariant] observedCommunity:
read = getObservedCommunity
write = setObservedCommunity
notify = observedCommunityChanged
proc joinCommunity*(self: ChatsView, communityId: string): string {.slot.} =
result = ""
try:
self.status.chat.joinCommunity(communityId)
self.joinedCommunityList.addCommunityItemToList(self.communityList.getCommunityById(communityId))
self.setActiveCommunity(communityId)
except Exception as e:
error "Error joining the community", msg = e.msg
result = fmt"Error joining the community: {e.msg}"
proc leaveCommunity*(self: ChatsView, communityId: string): string {.slot.} =
result = ""
try:
self.status.chat.leaveCommunity(communityId)
if (communityId == self.activeCommunity.communityItem.id):
self.activeCommunity.setActive(false)
self.joinedCommunityList.removeCommunityItemFromList(communityId)
except Exception as e:
error "Error leaving the community", msg = e.msg
result = fmt"Error leaving the community: {e.msg}"
proc leaveCurrentCommunity*(self: ChatsView): string {.slot.} =
result = self.leaveCommunity(self.activeCommunity.communityItem.id)

View File

@ -18,6 +18,7 @@ type
ContentType = UserRole + 9
Muted = UserRole + 10
Id = UserRole + 11
Description = UserRole + 12
QtObject:
type
@ -75,6 +76,7 @@ QtObject:
of ChannelsRoles.HasMentions: result = newQVariant(chatItem.hasMentions)
of ChannelsRoles.Muted: result = newQVariant(chatItem.muted.bool)
of ChannelsRoles.Id: result = newQVariant($chatItem.id)
of ChannelsRoles.Description: result = newQVariant(chatItem.description)
method roleNames(self: ChannelsList): Table[int, string] =
{
@ -88,9 +90,15 @@ QtObject:
ChannelsRoles.HasMentions.int: "hasMentions",
ChannelsRoles.ContentType.int: "contentType",
ChannelsRoles.Muted.int: "muted",
ChannelsRoles.Id.int: "id"
ChannelsRoles.Id.int: "id",
ChannelsRoles.Description.int: "description"
}.toTable
proc setChats*(self: ChannelsList, chats: seq[Chat]) =
self.beginResetModel()
self.chats = chats
self.endResetModel()
proc addChatItemToList*(self: ChannelsList, channel: Chat): int =
self.beginInsertRows(newQModelIndex(), 0, 0)
self.chats.insert(channel, 0)
@ -149,7 +157,7 @@ QtObject:
else:
self.chats[0] = channel
self.dataChanged(topLeft, bottomRight, @[ChannelsRoles.Name.int, ChannelsRoles.ContentType.int, ChannelsRoles.LastMessage.int, ChannelsRoles.Timestamp.int, ChannelsRoles.UnreadMessages.int, ChannelsRoles.Identicon.int, ChannelsRoles.ChatType.int, ChannelsRoles.Color.int, ChannelsRoles.HasMentions.int, ChannelsRoles.Muted.int])
self.dataChanged(topLeft, bottomRight, @[ChannelsRoles.Name.int, ChannelsRoles.Description.int, ChannelsRoles.ContentType.int, ChannelsRoles.LastMessage.int, ChannelsRoles.Timestamp.int, ChannelsRoles.UnreadMessages.int, ChannelsRoles.Identicon.int, ChannelsRoles.ChatType.int, ChannelsRoles.Color.int, ChannelsRoles.HasMentions.int, ChannelsRoles.Muted.int])
proc clearUnreadMessagesCount*(self: ChannelsList, channel: var Chat) =
let idx = self.chats.findIndexById(channel.id)
@ -161,7 +169,7 @@ QtObject:
channel.hasMentions = false
self.chats[idx] = channel
self.dataChanged(topLeft, bottomRight, @[ChannelsRoles.Name.int, ChannelsRoles.ContentType.int, ChannelsRoles.LastMessage.int, ChannelsRoles.Timestamp.int, ChannelsRoles.UnreadMessages.int, ChannelsRoles.Identicon.int, ChannelsRoles.ChatType.int, ChannelsRoles.Color.int, ChannelsRoles.HasMentions.int, ChannelsRoles.Muted.int])
self.dataChanged(topLeft, bottomRight, @[ChannelsRoles.Name.int, ChannelsRoles.Description.int, ChannelsRoles.ContentType.int, ChannelsRoles.LastMessage.int, ChannelsRoles.Timestamp.int, ChannelsRoles.UnreadMessages.int, ChannelsRoles.Identicon.int, ChannelsRoles.ChatType.int, ChannelsRoles.Color.int, ChannelsRoles.HasMentions.int, ChannelsRoles.Muted.int])
proc renderInline(self: ChannelsList, elem: TextItem): string =
case elem.textType:

View File

@ -0,0 +1,85 @@
import NimQml, Tables, std/wrapnils
import ../../../status/[chat/chat, status]
import channels_list
import ../../../eventemitter
QtObject:
type CommunityItemView* = ref object of QObject
communityItem*: Community
chats*: ChannelsList
status*: Status
active*: bool
proc setup(self: CommunityItemView) =
self.QObject.setup
proc delete*(self: CommunityItemView) =
if not self.chats.isNil: self.chats.delete
self.QObject.delete
proc newCommunityItemView*(status: Status): CommunityItemView =
new(result, delete)
result = CommunityItemView()
result.status = status
result.active = false
result.chats = newChannelsList(status)
result.setup
proc setCommunityItem*(self: CommunityItemView, communityItem: Community) =
self.communityItem = communityItem
self.chats.setChats(communityItem.chats)
proc activeChanged*(self: CommunityItemView) {.signal.}
proc setActive*(self: CommunityItemView, value: bool) {.slot.} =
self.active = value
self.status.events.emit("communityActiveChanged", Args())
self.activeChanged()
proc active*(self: CommunityItemView): bool {.slot.} = result = ?.self.active
QtProperty[bool] active:
read = active
write = setActive
notify = activeChanged
proc id*(self: CommunityItemView): string {.slot.} = result = ?.self.communityItem.id
QtProperty[string] id:
read = id
proc name*(self: CommunityItemView): string {.slot.} = result = ?.self.communityItem.name
QtProperty[string] name:
read = name
proc description*(self: CommunityItemView): string {.slot.} = result = ?.self.communityItem.description
QtProperty[string] description:
read = description
proc access*(self: CommunityItemView): int {.slot.} = result = ?.self.communityItem.access
QtProperty[int] access:
read = access
proc admin*(self: CommunityItemView): bool {.slot.} = result = ?.self.communityItem.admin
QtProperty[bool] admin:
read = admin
proc joined*(self: CommunityItemView): bool {.slot.} = result = ?.self.communityItem.joined
QtProperty[bool] joined:
read = joined
proc verified*(self: CommunityItemView): bool {.slot.} = result = ?.self.communityItem.verified
QtProperty[bool] verified:
read = verified
proc getChats*(self: CommunityItemView): QVariant {.slot.} =
result = newQVariant(self.chats)
QtProperty[QVariant] chats:
read = getChats

View File

@ -0,0 +1,97 @@
import NimQml, Tables
import ../../../status/chat/[chat, message]
import ../../../status/status
import ../../../status/ens
import ../../../status/accounts
import strutils
type
CommunityRoles {.pure.} = enum
Id = UserRole + 1,
Name = UserRole + 2
Description = UserRole + 3
# Color = UserRole + 4
Access = UserRole + 5
Admin = UserRole + 6
Joined = UserRole + 7
Verified = UserRole + 8
QtObject:
type
CommunityList* = ref object of QAbstractListModel
communities*: seq[Community]
status: Status
fetched*: bool
proc setup(self: CommunityList) = self.QAbstractListModel.setup
proc delete(self: CommunityList) =
self.communities = @[]
self.QAbstractListModel.delete
proc newCommunityList*(status: Status): CommunityList =
new(result, delete)
result.communities = @[]
result.status = status
result.setup()
method rowCount*(self: CommunityList, index: QModelIndex = nil): int = self.communities.len
method data(self: CommunityList, index: QModelIndex, role: int): QVariant =
if not index.isValid:
return
if index.row < 0 or index.row >= self.communities.len:
return
let communityItem = self.communities[index.row]
let communityItemRole = role.CommunityRoles
case communityItemRole:
of CommunityRoles.Name: result = newQVariant(communityItem.name)
of CommunityRoles.Description: result = newQVariant(communityItem.description)
of CommunityRoles.Id: result = newQVariant(communityItem.id)
# of CommunityRoles.Color: result = newQVariant(communityItem.color)
of CommunityRoles.Access: result = newQVariant(communityItem.access.int)
of CommunityRoles.Admin: result = newQVariant(communityItem.admin.bool)
of CommunityRoles.Joined: result = newQVariant(communityItem.joined.bool)
of CommunityRoles.Verified: result = newQVariant(communityItem.verified.bool)
method roleNames(self: CommunityList): Table[int, string] =
{
CommunityRoles.Name.int:"name",
CommunityRoles.Description.int:"description",
CommunityRoles.Id.int: "id",
# CommunityRoles.Color.int: "color",
CommunityRoles.Access.int: "access",
CommunityRoles.Admin.int: "admin",
CommunityRoles.Verified.int: "verified",
CommunityRoles.Joined.int: "joined"
}.toTable
proc setNewData*(self: CommunityList, communityList: seq[Community]) =
self.beginResetModel()
self.communities = communityList
self.endResetModel()
proc addCommunityItemToList*(self: CommunityList, community: Community) =
self.beginInsertRows(newQModelIndex(), self.communities.len, self.communities.len)
self.communities.add(community)
self.endInsertRows()
proc removeCommunityItemFromList*(self: CommunityList, id: string) =
let idx = self.communities.findIndexById(id)
self.beginRemoveRows(newQModelIndex(), idx, idx)
self.communities.delete(idx)
self.endRemoveRows()
proc getCommunityById*(self: CommunityList, communityId: string): Community =
for community in self.communities:
if community.id == communityId:
return community
proc addChannelToCommunity*(self: CommunityList, communityId: string, chat: Chat) =
var community = self.getCommunityById(communityId)
community.chats.add(chat)
let index = self.communities.findIndexById(communityId)
self.communities[index] = community

View File

@ -373,3 +373,21 @@ proc requestTransaction*(self: ChatModel, chatId: string, fromAddress: string, a
let address = if (tokenAddress == constants.ZERO_ADDRESS): "" else: tokenAddress
let response = status_chat_commands.requestTransaction(chatId, fromAddress, amount, address)
discard self.processMessageUpdateAfterSend(response)
proc getAllComunities*(self: ChatModel): seq[Community] =
result = status_chat.getAllComunities()
proc getJoinedComunities*(self: ChatModel): seq[Community] =
result = status_chat.getJoinedComunities()
proc createCommunity*(self: ChatModel, name: string, description: string, color: string, image: string, access: int): Community =
result = status_chat.createCommunity(name, description, color, image, access)
proc createCommunityChannel*(self: ChatModel, communityId: string, name: string, description: string): Chat =
result = status_chat.createCommunityChannel(communityId, name, description)
proc joinCommunity*(self: ChatModel, communityId: string) =
status_chat.joinCommunity(communityId)
proc leaveCommunity*(self: ChatModel, communityId: string) =
status_chat.leaveCommunity(communityId)

View File

@ -8,6 +8,7 @@ type ChatType* {.pure.}= enum
PrivateGroupChat = 3,
Profile = 4,
Timeline = 5
CommunityChat = 6
proc isOneToOne*(self: ChatType): bool = self == ChatType.OneToOne
proc isTimeline*(self: ChatType): bool = self == ChatType.Timeline
@ -58,6 +59,7 @@ proc toJsonNode*(self: seq[ChatMembershipEvent]): seq[JsonNode] =
type Chat* = ref object
id*: string # ID is the id of the chat, for public chats it is the name e.g. status, for one-to-one is the hex encoded public key and for group chats is a random uuid appended with the hex encoded pk of the creator of the chat
name*: string
description*: string
color*: string
identicon*: string
isActive*: bool # indicates whether the chat has been soft deleted
@ -73,9 +75,30 @@ type Chat* = ref object
muted*: bool
ensName*: string
type CommunityAccessLevel* = enum
unknown = 0
public = 1
invitationOnly = 2
onRequest = 3
type Community* = object
id*: string
name*: string
description*: string
chats*: seq[Chat]
# members: seq[] # TODO find what goes in there
# color*: string
access*: int
admin*: bool
joined*: bool
verified*: bool
proc `$`*(self: Chat): string =
result = fmt"Chat(id:{self.id}, name:{self.name}, active:{self.isActive}, type:{self.chatType})"
proc `$`*(self: Community): string =
result = fmt"Community(id:{self.id}, name:{self.name}, description:{self.description}"
proc toJsonNode*(self: Chat): JsonNode =
result = %* {
"active": self.isActive,
@ -101,6 +124,15 @@ proc findIndexById*(self: seq[Chat], id: string): int =
result = idx
break
proc findIndexById*(self: seq[Community], id: string): int =
result = -1
var idx = -1
for item in self:
inc idx
if(item.id == id):
result = idx
break
proc isMember*(self: Chat, pubKey: string): bool =
for member in self.members:
if member.id == pubKey and member.joined: return true

View File

@ -12,6 +12,7 @@ type ContentType* {.pure.} = enum
Group = 6,
Image = 7,
Audio = 8
Community = 9
type TextItem* = object
textType*: string

View File

@ -51,8 +51,8 @@ proc loadChats*(): seq[Chat] =
if jsonResponse["result"].kind != JNull:
for jsonChat in jsonResponse{"result"}:
let chat = jsonChat.toChat
if chat.isActive and chat.chatType != ChatType.Unknown:
result.add(jsonChat.toChat)
if chat.isActive and chat.chatType != ChatType.Unknown and chat.chatType != ChatType.CommunityChat:
result.add(chat)
result.sort(sortChats)
proc chatMessages*(chatId: string, cursor: string = ""): (string, seq[Message]) =
@ -209,3 +209,90 @@ proc getLinkPreviewData*(link: string): JsonNode =
raise newException(RpcException, fmt"""Error getting link preview data for '{link}': {response.error.message}""")
response.result
proc getAllComunities*(): seq[Community] =
var communities: seq[Community] = @[]
let rpcResult = callPrivateRPC("communities".prefix).parseJSON()
debug "LIST", rpcResult
if rpcResult{"result"}.kind != JNull:
for jsonCommunity in rpcResult["result"]:
var community = jsonCommunity.toCommunity()
communities.add(community)
return communities
proc getJoinedComunities*(): seq[Community] =
var communities: seq[Community] = @[]
let rpcResult = callPrivateRPC("joinedCommunities".prefix).parseJSON()
debug "LIST", rpcResult
if rpcResult{"result"}.kind != JNull:
for jsonCommunity in rpcResult["result"]:
var community = jsonCommunity.toCommunity()
communities.add(community)
return communities
proc createCommunity*(name: string, description: string, color: string, image: string, access: int): Community =
let rpcResult = callPrivateRPC("createCommunity".prefix, %*[{
"permissions": {
"access": access
},
"identity": {
"display_name": name,
"description": description#,
# "color": color#,
# TODO add images once it is supported by Status-Go
# "images": [
# {
# "payload": image,
# # TODO get that from an enum
# "image_type": 1 # 1 is a raw payload
# }
# ]
}
}]).parseJSON()
debug "RESULT", rpcResult
if rpcResult{"result"}.kind != JNull:
result = rpcResult["result"]["communities"][0].toCommunity()
proc createCommunityChannel*(communityId: string, name: string, description: string): Chat =
let rpcResult = callPrivateRPC("createCommunityChat".prefix, %*[
communityId,
{
"permissions": {
"access": 1 # TODO get this from user selected privacy setting
},
"identity": {
"display_name": name,
"description": description#,
# "color": color#,
# TODO add images once it is supported by Status-Go
# "images": [
# {
# "payload": image,
# # TODO get that from an enum
# "image_type": 1 # 1 is a raw payload
# }
# ]
}
}]).parseJSON()
debug "RESULT", rpcResult
if rpcResult{"result"}.kind != JNull:
result = rpcResult["result"]["chats"][0].toChat()
#{\"jsonrpc\":\"2.0\",\"id\":0,\"result\":{\"chats\":[{\"id\":\"0x03537a54bd6f697f282ae848452f25ce656026cd8d0b3d3489178c76d3bf9ddf20cbf03afb-89ab-444e-96e9-e23b6c1266c0\",\"name\":\"general\",\"color\":\"#887af9\",\"active\":true,\"chatType\":6,\"timestamp\":1606424570375,\"lastClockValue\":0,\"deletedAtClockValue\":0,\"unviewedMessagesCount\":0,\"lastMessage\":null,\"members\":null,\"membershipUpdateEvents\":null,\"identicon\":\"\",\"communityId\":\"0x03537a54bd6f697f282ae848452f25ce656026cd8d0b3d3489178c76d3bf9ddf20\"}],\"communities\":[{\"id\":\"0x03537a54bd6f697f282ae848452f25ce656026cd8d0b3d3489178c76d3bf9ddf20\",\"description\":{\"clock\":2,\"permissions\":{\"access\":1},\"identity\":{\"display_name\":\"Jo2\",\"description\":\"Jo again\"},\"chats\":{\"cbf03afb-89ab-444e-96e9-e23b6c1266c0\":{\"permissions\":{\"access\":1},\"identity\":{\"display_name\":\"general\",\"description\":\"general channel\"}}}},\"admin\":true,\"verified\":false,\"joined\":true}],\"communitiesChanges\":[{\"MembersAdded\":{},\"MembersRemoved\":{},\"ChatsRemoved\":{},\"ChatsAdded\":{\"cbf03afb-89ab-444e-96e9-e23b6c1266c0\":{\"permissions\":{\"access\":1},\"identity\":{\"display_name\":\"general\",\"description\":\"general channel\"}}},\"ChatsModified\":{}}],\"filters\":[{\"chatId\":\"0x03537a54bd6f697f282ae848452f25ce656026cd8d0b3d3489178c76d3bf9ddf20cbf03afb-89ab-444e-96e9-e23b6c1266c0\",\"filterId\":\"eb220a77bde14d967462529d0ec7c1fa09a0ece4efebefb388ea6e0ecf9a1950\",\"symKeyId\":\"0f08b999ab6571429f79c4762bd922f2b7db38c80ba99cd4a5ac15ef75357d0e\",\"oneToOne\":false,\"identity\":\"\",\"topic\":\"0x46a1c2be\",\"discovery\":false,\"negotiated\":false,\"listen\":true}]}}
# if rpcResult{"result"}.kind != JNull:
# result = rpcResult["result"]["communities"][0].toCommunity()
proc joinCommunity*(communityId: string) =
let res = callPrivateRPC("joinCommunity".prefix, %*[communityId])#.parseJSON()["result"]
debug "RESULT", res
proc leaveCommunity*(communityId: string) =
let res = callPrivateRPC("leaveCommunity".prefix, %*[communityId])#.parseJSON()["result"]
debug "RESULT", res

View File

@ -152,6 +152,31 @@ proc toChat*(jsonChat: JsonNode): Chat =
for jsonMember in jsonChat["membershipUpdateEvents"]:
result.membershipUpdateEvents.add(jsonMember.toChatMembershipEvent)
proc toCommunity*(jsonCommunity: JsonNode): Community =
result = Community(
id: jsonCommunity{"id"}.getStr,
name: jsonCommunity{"description"}{"identity"}{"display_name"}.getStr,
description: jsonCommunity{"description"}{"identity"}{"description"}.getStr,
# color: jsonCommunity{"description"}{"identity"}{"color"}.getStr,
access: jsonCommunity{"description"}{"permissions"}{"access"}.getInt,
admin: jsonCommunity{"admin"}.getBool,
joined: jsonCommunity{"joined"}.getBool,
verified: jsonCommunity{"verified"}.getBool,
chats: newSeq[Chat]()
)
if not jsonCommunity["description"].hasKey("chats") or jsonCommunity["description"]["chats"].kind == JNull:
return result
for chatId, chat in jsonCommunity{"description"}{"chats"}:
result.chats.add(Chat(
id: chatId,
name: chat{"identity"}{"display_name"}.getStr,
description: chat{"identity"}{"description"}.getStr,
# TODO get this from access
chatType: ChatType.Public#chat{"permissions"}{"access"}.getInt,
))
proc toTextItem*(jsonText: JsonNode): TextItem =
result = TextItem(
literal: jsonText{"literal"}.getStr,

View File

@ -17,6 +17,12 @@ SplitView {
chatColumn.onActivated()
}
function openPopup(popupComponent, params = {}) {
const popup = popupComponent.createObject(chatView, params);
popup.open()
return popup
}
Connections {
target: applicationWindow
onSettingsLoaded: {
@ -26,16 +32,32 @@ SplitView {
}
Component.onDestruction: appSettings.chatSplitView = this.saveState()
ContactsColumn {
id: contactsColumn
SplitView.preferredWidth: Style.current.leftTabPrefferedSize
SplitView.minimumWidth: Style.current.leftTabMinimumWidth
SplitView.maximumWidth: Style.current.leftTabMaximumWidth
Loader {
id: contactColumnLoader
sourceComponent: chatsModel.activeCommunity.active ? communtiyColumnComponent : contactsColumnComponent
}
Component {
id: contactsColumnComponent
ContactsColumn {
SplitView.preferredWidth: Style.current.leftTabPrefferedSize
SplitView.minimumWidth: Style.current.leftTabMinimumWidth
SplitView.maximumWidth: Style.current.leftTabMaximumWidth
}
}
Component {
id: communtiyColumnComponent
CommunityColumn {
SplitView.preferredWidth: Style.current.leftTabPrefferedSize
SplitView.minimumWidth: Style.current.leftTabMinimumWidth
SplitView.maximumWidth: Style.current.leftTabMaximumWidth
}
}
ChatColumn {
id: chatColumn
chatGroupsListViewCount: contactsColumn.chatGroupsListViewCount
chatGroupsListViewCount: contactColumnLoader.item.chatGroupsListViewCount
}
function openProfilePopup(userNameParam, fromAuthorParam, identiconParam, textParam, nicknameParam, parentPopup){

View File

@ -0,0 +1,147 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import "../../../imports"
import "../../../shared"
import "../../../shared/status"
import "./ContactsColumn"
import "./ContactsColumn/CommunityComponents"
Item {
// TODO unhardcode
property int chatGroupsListViewCount: channelList.channelListCount
id: root
Layout.fillHeight: true
Component {
id: createChannelPopup
CreateChannelPopup {
onClosed: {
destroy()
}
}
}
Item {
id: communityHeader
width: parent.width
height: communityImage.height
anchors.top: parent.top
anchors.topMargin: Style.current.padding
StatusIconButton {
id: backArrow
icon.name: "arrow-right"
iconRotation: 180
iconColor: Style.current.inputColor
anchors.left: parent.left
anchors.leftMargin: Style.current.bigPadding
anchors.verticalCenter: parent.verticalCenter
onClicked: chatsModel.activeCommunity.active = false
}
RoundedImage {
id: communityImage
width: 40
height: 40
// TODO get the real image once it's available
source: "../../img/ens-header-dark@2x.png"
anchors.left: backArrow.right
anchors.leftMargin: Style.current.smallPadding
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: communityName
text: chatsModel.activeCommunity.name
anchors.left: communityImage.right
anchors.leftMargin: Style.current.halfPadding
font.pixelSize: 15
font.weight: Font.Medium
}
StyledText {
id: communityNbMember
// TOD get real numbers
text: qsTr("%1 members").arg(12)
anchors.left: communityName.left
anchors.bottom: parent.bottom
font.pixelSize: 12
font.weight: Font.Thin
color: Style.current.secondaryText
}
StatusIconButton {
id: optionsBtn
icon.name: "dots-icon"
iconColor: Style.current.inputColor
anchors.right: parent.right
anchors.rightMargin: Style.current.bigPadding
anchors.verticalCenter: parent.verticalCenter
onClicked: {
optionsMenu.open()
}
}
PopupMenu {
id: optionsMenu
x: optionsBtn.x + optionsBtn.width / 2 - optionsMenu.width / 2
y: optionsBtn.height
Action {
enabled: chatsModel.activeCommunity.admin
text: qsTrId("Create channel")
icon.source: "../../img/hash.svg"
icon.width: 20
icon.height: 20
onTriggered: openPopup(createChannelPopup, {communityId: chatsModel.activeCommunity.id})
}
Action {
text: qsTrId("Leave community")
icon.source: "../../img/delete.svg"
icon.color: Style.current.red
icon.width: 20
icon.height: 20
onTriggered: chatsModel.leaveCurrentCommunity()
}
}
}
ScrollView {
id: chatGroupsContainer
anchors.top: communityHeader.bottom
anchors.topMargin: Style.current.padding
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
leftPadding: Style.current.halfPadding
rightPadding: Style.current.halfPadding
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
contentHeight: channelList.height + emptyViewAndSuggestions.height + 2 * Style.current.padding
clip: true
ChannelList {
id: channelList
searchStr: ""
channelModel: chatsModel.activeCommunity.chats
}
CommunityWelcomeBanner {
id: emptyViewAndSuggestions
visible: chatsModel.activeCommunity.admin
width: parent.width
anchors.top: channelList.bottom
anchors.topMargin: Style.current.padding
}
}
}
/*##^##
Designer {
D{i:0;autoSize:true;formeditorColor:"#ffffff";height:480;width:640}
}
##^##*/

View File

@ -25,16 +25,58 @@ Item {
font.pixelSize: 17
}
PublicChatPopup {
id: publicChatPopup
Component {
id: publicChatPopupComponent
PublicChatPopup {
onClosed: {
destroy()
}
}
}
GroupChatPopup {
id: groupChatPopup
Component {
id: groupChatPopupComponent
GroupChatPopup {
onClosed: {
destroy()
}
}
}
PrivateChatPopup {
id: privateChatPopup
Component {
id: privateChatPopupComponent
PrivateChatPopup {
onClosed: {
destroy()
}
}
}
Component {
id: communitiesPopupComponent
CommunitiesPopup {
onClosed: {
destroy()
}
}
}
Component {
id: createCommunitiesPopupComponent
CreateCommunityPopup {
onClosed: {
destroy()
}
}
}
Component {
id: communityDetailPopup
CommunityDetailPopup {
onClosed: {
destroy()
}
}
}
SearchBox {
@ -55,15 +97,45 @@ Item {
anchors.topMargin: Style.current.padding
}
ChannelList {
id: channelList
searchStr: contactsColumn.searchStr
ScrollView {
id: chatGroupsContainer
anchors.top: searchBox.bottom
anchors.topMargin: Style.current.padding
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.top: searchBox.bottom
anchors.topMargin: Style.current.padding
leftPadding: Style.current.halfPadding
rightPadding: Style.current.halfPadding
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
contentHeight: communityList.height + channelList.height + 2 * Style.current.padding + emptyViewAndSuggestions.height
clip: true
CommunityList {
id: communityList
searchStr: contactsColumn.searchStr
}
Separator {
id: communitySep
visible: communityList.visible
anchors.top: communityList.bottom
anchors.topMargin: Style.current.halfPadding
}
ChannelList {
id: channelList
anchors.top: communitySep.bottom
anchors.topMargin: Style.current.halfPadding
searchStr: contactsColumn.searchStr
channelModel: chatsModel.chats
}
EmptyView {
id: emptyViewAndSuggestions
width: parent.width
anchors.top: channelList.bottom
anchors.topMargin: Style.current.smallPadding
}
}
}

View File

@ -27,7 +27,7 @@ StatusRoundButton {
icon.source: "../../../img/new_chat.svg"
icon.width: 20
icon.height: 20
onTriggered: privateChatPopup.open()
onTriggered: openPopup(groupChatPopupComponent)
}
Action {
//% "Start group chat"
@ -35,7 +35,7 @@ StatusRoundButton {
icon.source: "../../../img/group_chat.svg"
icon.width: 20
icon.height: 20
onTriggered: groupChatPopup.open()
onTriggered: openPopup(groupChatPopupComponent)
}
Action {
//% "Join public chat"
@ -43,7 +43,16 @@ StatusRoundButton {
icon.source: "../../../img/public_chat.svg"
icon.width: 20
icon.height: 20
onTriggered: publicChatPopup.open()
onTriggered: openPopup(publicChatPopupComponent)
}
Action {
text: qsTr("Communities")
icon.source: "../../../img/communities.svg"
icon.width: 20
icon.height: 20
onTriggered: {
openPopup(communitiesPopupComponent)
}
}
onAboutToHide: {
btnAdd.state = "default"

View File

@ -9,7 +9,7 @@ Rectangle {
property string chatId: ""
property string name: "channelName"
property string lastMessage: "My latest message\n with a return"
property string timestamp: "20/2/2020"
property string timestamp: "1605212622434"
property string unviewedMessagesCount: "2"
property string identicon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAGQAQMAAAC6caSPAAAABlBMVEXMzMz////TjRV2AAAAAWJLR0QB/wIt3gAAACpJREFUGBntwYEAAAAAw6D7Uw/gCtUAAAAAAAAAAAAAAAAAAAAAAAAAgBNPsAABAjKCqQAAAABJRU5ErkJggg=="
property bool hasMentions: false

View File

@ -6,93 +6,73 @@ import "../../../../imports"
import "../components"
import "./"
ScrollView {
Rectangle {
property var channelModel
property alias channelListCount: chatGroupsListView.count
property string searchStr: ""
id: chatGroupsContainer
Layout.fillHeight: true
Layout.fillWidth: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
contentHeight: channelListContent.height + Style.current.padding
clip: true
id: channelListContent
width: parent.width
height: childrenRect.height
Timer {
id: timer
}
Item {
id: channelListContent
Layout.fillHeight: true
ListView {
id: chatGroupsListView
spacing: Style.current.halfPadding
anchors.top: parent.top
height: childrenRect.height
visible: height > 0
anchors.right: parent.right
anchors.left: parent.left
height: childrenRect.height
ListView {
id: chatGroupsListView
anchors.top: parent.top
height: childrenRect.height
visible: height > 0
anchors.right: parent.right
anchors.left: parent.left
anchors.rightMargin: Style.current.padding
anchors.leftMargin: Style.current.padding
interactive: false
model: chatsModel.chats
delegate: Channel {
name: model.name
muted: model.muted
lastMessage: model.lastMessage
timestamp: model.timestamp
chatType: model.chatType
identicon: model.identicon
unviewedMessagesCount: model.unviewedMessagesCount
hasMentions: model.hasMentions
contentType: model.contentType
searchStr: chatGroupsContainer.searchStr
chatId: model.id
}
onCountChanged: {
if (count > 0 && chatsModel.activeChannelIndex > -1) {
// If a chat is added or removed, we set the current index to the first value
chatsModel.activeChannelIndex = 0;
currentIndex = 0;
interactive: false
model: channelListContent.channelModel
delegate: Channel {
name: model.name
muted: model.muted
lastMessage: model.lastMessage
timestamp: model.timestamp
chatType: model.chatType
identicon: model.identicon
unviewedMessagesCount: model.unviewedMessagesCount
hasMentions: model.hasMentions
contentType: model.contentType
searchStr: channelListContent.searchStr
chatId: model.id
}
onCountChanged: {
if (count > 0 && chatsModel.activeChannelIndex > -1) {
// If a chat is added or removed, we set the current index to the first value
chatsModel.activeChannelIndex = 0;
currentIndex = 0;
} else {
if (chatsModel.activeChannelIndex > -1) {
chatGroupsListView.currentIndex = 0;
} else {
if(chatsModel.activeChannelIndex > -1){
chatGroupsListView.currentIndex = 0;
} else {
// Initial state. No chat has been selected yet
chatGroupsListView.currentIndex = -1;
}
// Initial state. No chat has been selected yet
chatGroupsListView.currentIndex = -1;
}
}
}
}
Rectangle {
id: noSearchResults
anchors.top: parent.top
height: 300
color: "transparent"
visible: !chatGroupsListView.visible && chatGroupsContainer.searchStr !== ""
anchors.left: parent.left
anchors.right: parent.right
Rectangle {
id: noSearchResults
anchors.top: parent.top
height: visible ? 300 : 0
color: "transparent"
visible: !chatGroupsListView.visible && channelListContent.searchStr !== ""
anchors.left: parent.left
anchors.right: parent.right
StyledText {
font.pixelSize: 15
color: Style.current.darkGrey
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("No search results")
}
StyledText {
font.pixelSize: 15
color: Style.current.darkGrey
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("No search results")
}
EmptyView {
width: parent.width
anchors.top: noSearchResults.visible ? noSearchResults.bottom : chatGroupsListView.bottom
anchors.topMargin: Style.current.smallPadding
}
}
GroupInfoPopup {
@ -252,7 +232,10 @@ ScrollView {
chatColumn.isReply = false;
}
}
}
/*##^##
Designer {
D{i:0;autoSize:true;height:480;width:640}

View File

@ -0,0 +1,99 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import "../../../../shared"
import "../../../../shared/status"
import "../../../../imports"
import "../components"
Rectangle {
property string communityId: ""
property string name: "channelName"
property string unviewedMessagesCount: "2"
property string image: "../../../img/ens-header-dark@2x.png"
property bool hasMentions: false
property string searchStr: ""
property bool isCompact: appSettings.compactMode
property bool hovered: false
id: wrapper
color: {
if (wrapper.hovered) {
return Style.current.secondaryBackground
}
return Style.current.transparent
}
anchors.right: parent.right
anchors.top: applicationWindow.top
anchors.left: parent.left
radius: Style.current.radius
// Hide the box if it is filtered out
property bool isVisible: searchStr === "" || name.includes(searchStr)
visible: isVisible ? true : false
height: isVisible ? !isCompact ? 64 : communityImage.height + Style.current.smallPadding * 2 : 0
RoundedImage {
id: communityImage
height: !isCompact ? 40 : 20
width: !isCompact ? 40 : 20
source: wrapper.image
anchors.left: parent.left
anchors.leftMargin: !isCompact ? Style.current.padding : Style.current.smallPadding
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: contactInfo
text: wrapper.name
anchors.right: contactNumberChatsCircle.left
anchors.rightMargin: Style.current.smallPadding
elide: Text.ElideRight
font.weight: Font.Medium
font.pixelSize: 15
anchors.left: communityImage.right
anchors.leftMargin: Style.current.padding
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
id: contactNumberChatsCircle
width: 22
height: 22
radius: 50
anchors.right: parent.right
anchors.rightMargin: !isCompact ? Style.current.padding : Style.current.smallPadding
anchors.verticalCenter: parent.verticalCenter
color: Style.current.primary
visible: (unviewedMessagesCount > 0) || wrapper.hasMentions
StyledText {
id: contactNumberChats
text: wrapper.hasMentions ? '@' : (wrapper.unviewedMessagesCount < 100 ? wrapper.unviewedMessagesCount : "99")
font.pixelSize: 12
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
color: Style.current.white
}
}
MouseArea {
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
anchors.fill: parent
hoverEnabled: true
onEntered: {
wrapper.hovered = true
}
onExited: {
wrapper.hovered = false
}
onClicked: {
chatsModel.setActiveCommunity(communityId)
}
}
}
/*##^##
Designer {
D{i:0;formeditorColor:"#ffffff";height:64;width:640}
}
##^##*/

View File

@ -0,0 +1,149 @@
import QtQuick 2.12
import QtQuick.Controls 2.3
import QtGraphicalEffects 1.13
import QtQuick.Dialogs 1.3
import "../../../../../imports"
import "../../../../../shared"
import "../../../../../shared/status"
ModalPopup {
property string communityId
readonly property int maxDescChars: 140
property string nameValidationError: ""
id: popup
height: 600
onOpened: {
nameInput.text = "";
nameInput.forceActiveFocus(Qt.MouseFocusReason)
}
function validate() {
nameValidationError = ""
if (nameInput.text === "") {
nameValidationError = qsTr("You need to enter a name")
} else if (!(/^[a-z0-9\-\ ]+$/i.test(nameInput.text))) {
nameValidationError = qsTr("Please restrict your name to letters, numbers, dashes and spaces")
} else if (nameInput.text.length > 100) {
nameValidationError = qsTr("Your name needs to be 100 characters or shorter")
}
return !nameValidationError && !descriptionTextArea.validationError
}
title: qsTr("New channel")
ScrollView {
property ScrollBar vScrollBar: ScrollBar.vertical
id: scrollView
anchors.fill: parent
rightPadding: Style.current.padding
anchors.rightMargin: - Style.current.halfPadding
contentHeight: content.height
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
clip: true
function scrollBackUp() {
vScrollBar.setPosition(0)
}
Item {
id: content
height: childrenRect.height
width: parent.width
Input {
id: nameInput
label: qsTr("Channel name")
placeholderText: qsTr("A cool name")
validationError: popup.nameValidationError
}
StyledTextArea {
id: descriptionTextArea
label: qsTr("Channel description")
placeholderText: qsTr("What your channel is about")
validationError: descriptionTextArea.text.length > maxDescChars ? qsTr("The description cannot exceed %1 characters").arg(maxDescChars) : ""
anchors.top: nameInput.bottom
anchors.topMargin: Style.current.bigPadding
customHeight: 88
}
StyledText {
id: charLimit
text: `${descriptionTextArea.text.length}/${maxDescChars}`
anchors.top: descriptionTextArea.bottom
anchors.topMargin: !descriptionTextArea.validationError ? 5 : - Style.current.smallPadding
anchors.right: descriptionTextArea.right
font.pixelSize: 12
color: !descriptionTextArea.validationError ? Style.current.textColor : Style.current.danger
}
Separator {
id: separator1
anchors.top: charLimit.bottom
anchors.topMargin: Style.current.bigPadding
}
Item {
id: privateSwitcher
height: privateSwitch.height
width: parent.width
anchors.top: separator1.bottom
anchors.topMargin: Style.current.smallPadding * 2
StyledText {
text: qsTr("Private channel")
anchors.verticalCenter: parent.verticalCenter
}
StatusSwitch {
id: privateSwitch
anchors.right: parent.right
}
}
StyledText {
id: privateExplanation
anchors.top: privateSwitcher.bottom
wrapMode: Text.WordWrap
anchors.topMargin: Style.current.smallPadding * 2
width: parent.width
text: qsTr("By making a channel private, only members with selected permission will be able to access it")
}
}
}
footer: StatusButton {
text: qsTr("Create")
anchors.right: parent.right
onClicked: {
if (!validate()) {
scrollView.scrollBackUp()
return
}
const error = chatsModel.createCommunityChannel(communityId,
Utils.filterXSS(nameInput.text),
Utils.filterXSS(descriptionTextArea.text))
if (error) {
creatingError.text = error
return creatingError.open()
}
// TODO Open the community once we have designs for it
popup.close()
}
MessageDialog {
id: creatingError
title: qsTr("Error creating the community")
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok
}
}
}

View File

@ -0,0 +1,78 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import "../../../../shared"
import "../../../../imports"
import "../components"
import "./"
Item {
property string searchStr: ""
id: root
width: parent.width
height: childrenRect.height
visible: communityListView.visible
ListView {
id: communityListView
spacing: Style.current.halfPadding
anchors.top: parent.top
height: childrenRect.height
// FIXME the height doesn't update
// visible: height > 0
width:parent.width
interactive: false
model: chatsModel.joinedCommunities
delegate: CommunityButton {
communityId: model.id
name: model.name
// TODO add other properties
searchStr: root.searchStr
}
}
Item {
id: noSearchResults
anchors.top: parent.top
height: visible ? 200 : 0
visible: !communityListView.visible && root.searchStr !== ""
width: parent.width
StyledText {
font.pixelSize: 15
color: Style.current.darkGrey
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("No search results in Communities")
}
}
// Connections {
// target: chatsModel.chats
// onDataChanged: {
// // If the current active channel receives messages and changes its position,
// // refresh the currentIndex accordingly
// if(chatsModel.activeChannelIndex !== communityListView.currentIndex){
// communityListView.currentIndex = chatsModel.activeChannelIndex
// }
// }
// }
// Connections {
// target: chatsModel
// onActiveChannelChanged: {
// chatsModel.hideLoadingIndicator()
// communityListView.currentIndex = chatsModel.activeChannelIndex
// SelectedMessage.reset();
// chatColumn.isReply = false;
// }
// }
}
/*##^##
Designer {
D{i:0;autoSize:true;height:480;width:640}
}
##^##*/

View File

@ -0,0 +1,81 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtGraphicalEffects 1.13
import "../../../../shared"
import "../../../../shared/status"
import "../../../../imports"
Rectangle {
id: root
height: visible ? 220 : 0
anchors.left: parent.left
anchors.leftMargin: Style.current.padding
anchors.right: parent.right
anchors.rightMargin: Style.current.padding
border.color: Style.current.border
radius: 16
color: Style.current.transparent
SVGImage {
anchors.top: parent.top
anchors.topMargin: -6
anchors.horizontalCenter: parent.horizontalCenter
source: "../../../img/chatEmptyHeader.svg"
width: 66
height: 50
}
StatusIconButton {
icon.name: "close"
id: closeImg
anchors.top: parent.top
anchors.topMargin: 10
anchors.right: parent.right
anchors.rightMargin: 10
icon.height: 20
icon.width: 20
iconColor: Style.current.darkGrey
onClicked: {
// TODO make this saved in the settings
root.visible = false
}
}
StyledText {
id: welcomeText
text: qsTr("Welcome to your community! ")
anchors.top: parent.top
anchors.topMargin: 60
horizontalAlignment: Text.AlignHCenter
font.pixelSize: 15
wrapMode: Text.WordWrap
anchors.right: parent.right
anchors.rightMargin: Style.current.xlPadding
anchors.left: parent.left
anchors.leftMargin: Style.current.xlPadding
}
StatusButton {
id: addMembersBtn
text: qsTr("Add members")
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: manageBtn.top
anchors.bottomMargin: Style.current.halfPadding
onClicked: {
console.log('ADD')
}
}
StatusButton {
id: manageBtn
text: qsTr("Manage community")
type: "secondary"
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Style.current.padding
onClicked: {
console.log('Manage')
}
}
}

View File

@ -0,0 +1,104 @@
import QtQuick 2.12
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
import QtQml.Models 2.3
import QtGraphicalEffects 1.13
import "../../../../imports"
import "../../../../shared"
import "../../../../shared/status"
ModalPopup {
id: popup
onOpened: {
searchBox.text = "";
searchBox.forceActiveFocus(Qt.MouseFocusReason)
}
title: qsTr("Communities")
SearchBox {
id: searchBox
iconWidth: 17
iconHeight: 17
customHeight: 36
fontPixelSize: 15
}
ScrollView {
id: scrollView
width: parent.width
anchors.topMargin: Style.current.padding
anchors.top: searchBox.bottom
anchors.bottom: parent.bottom
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: communitiesList.contentHeight > communitiesList.height ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff
ListView {
anchors.fill: parent
model: chatsModel.communities
spacing: 4
clip: true
id: communitiesList
delegate: Item {
// TODO add the serach for the name and category once they exist
visible: !searchBox.text || description.includes(searchBox.text)
height: visible ? communityImage.height + Style.current.smallPadding : 0
width: parent.width
RoundedImage {
id: communityImage
width: 40
height: 40
// TODO get the real image once it's available
source: "../../../img/ens-header-dark@2x.png"
}
StyledText {
id: communityName
text: name
anchors.left: communityImage.right
anchors.leftMargin: Style.current.padding
font.pixelSize: 17
font.weight: Font.Bold
}
StyledText {
id: communityDesc
text: description
anchors.left: communityName.left
anchors.right: parent.right
anchors.top: communityName.bottom
font.pixelSize: 15
font.weight: Font.Thin
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
// TODO if already joined, just open the Community in the section
if (joined) {
chatsModel.setActiveCommunity(id)
} else {
chatsModel.setObservedCommunity(id)
openPopup(communityDetailPopup)
}
popup.close()
}
}
}
}
}
footer: StatusButton {
text: qsTr("Create a community")
anchors.right: parent.right
onClicked: {
openPopup(createCommunitiesPopupComponent)
popup.close()
}
}
}

View File

@ -0,0 +1,175 @@
import QtQuick 2.12
import QtQuick.Dialogs 1.3
import "../../../../imports"
import "../../../../shared"
import "../../../../shared/status"
import "../ContactsColumn"
ModalPopup {
property QtObject community: chatsModel.observedCommunity
property string communityId: community.id
property string name: community.name
property string description: community.description
// TODO get the real image once it's available
property string source: "../../../img/ens-header-dark@2x.png"
// TODO set real nb of members
property int nbMembers: 12
id: popup
header: Item {
height: childrenRect.height
width: parent.width
RoundedImage {
id: communityImg
source: popup.source
width: 40
height: 40
}
StyledTextEdit {
id: communityName
text: popup.name
anchors.top: parent.top
anchors.topMargin: 2
anchors.left: communityImg.right
anchors.leftMargin: Style.current.smallPadding
font.bold: true
font.pixelSize: 17
readOnly: true
}
StyledText {
// TODO get this from access property
text: qsTr("Public community")
anchors.left: communityName.left
anchors.top: communityName.bottom
anchors.topMargin: 2
font.pixelSize: 15
font.weight: Font.Thin
color: Style.current.secondaryText
}
}
StyledText {
id: descriptionText
text: popup.description
wrapMode: Text.WrapAnywhere
width: parent.width
font.pixelSize: 15
font.weight: Font.Thin
}
Item {
id: memberContainer
width: parent.width
height: memberImage.height
anchors.top: descriptionText.bottom
anchors.topMargin: Style.current.padding
SVGImage {
id: memberImage
source: "../../../img/member.svg"
width: 16
height: 16
}
StyledText {
text: qsTr("%1 members").arg(popup.nbMembers)
wrapMode: Text.WrapAnywhere
width: parent.width
anchors.left: memberImage.right
anchors.leftMargin: 4
font.pixelSize: 15
font.weight: Font.Medium
}
}
Separator {
id: sep1
anchors.left: parent.left
anchors.right: parent.right
anchors.top: memberContainer.bottom
anchors.topMargin: Style.current.smallPadding
anchors.leftMargin: -Style.current.padding
anchors.rightMargin: -Style.current.padding
}
StyledText {
id: chatsTitle
text: qsTr("Chats")
anchors.top: sep1.bottom
anchors.topMargin: Style.current.bigPadding
font.pixelSize: 15
font.weight: Font.Thin
}
ListView {
id: chatsList
width: parent.width
anchors.top: chatsTitle.bottom
anchors.topMargin: 4
anchors.bottom: parent.bottom
clip: true
model: community.chats
delegate: Channel {
id: channelItem
unviewedMessagesCount: ""
width: parent.width
name: model.name
lastMessage: model.description
contentType: Constants.messageType
border.width: 0
color: Style.current.transparent
}
}
footer: Item {
anchors.fill: parent
StatusIconButton {
id: backButton
icon.name: "leave_chat"
width: 44
height: 44
iconColor: Style.current.primary
highlighted: true
icon.color: Style.current.primary
icon.width: 28
icon.height: 28
radius: width / 2
onClicked: {
openPopup(communitiesPopupComponent)
popup.close()
}
}
StatusButton {
text: qsTr("Join %1").arg(popup.name)
anchors.right: parent.right
onClicked: {
const error = chatsModel.joinCommunity(popup.communityId)
if (error) {
joiningError.text = error
return joiningError.open()
}
popup.close()
}
}
MessageDialog {
id: joiningError
title: qsTr("Error joining the community")
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok
}
}
}

View File

@ -0,0 +1,301 @@
import QtQuick 2.12
import QtQuick.Controls 2.3
import QtGraphicalEffects 1.13
import QtQuick.Dialogs 1.3
import "../../../../imports"
import "../../../../shared"
import "../../../../shared/status"
ModalPopup {
readonly property int maxDescChars: 140
property string nameValidationError: ""
property string colorValidationError: ""
property string selectedImageValidationError: ""
property string selectedImage: ""
id: popup
height: 600
onOpened: {
nameInput.text = "";
nameInput.forceActiveFocus(Qt.MouseFocusReason)
}
function validate() {
nameValidationError = ""
colorValidationError = ""
selectedImageValidationError = ""
if (nameInput.text === "") {
nameValidationError = qsTr("You need to enter a name")
} else if (!(/^[a-z0-9\-\ ]+$/i.test(nameInput.text))) {
nameValidationError = qsTr("Please restrict your name to letters, numbers, dashes and spaces")
} else if (nameInput.text.length > 100) {
nameValidationError = qsTr("Your name needs to be 100 characters or shorter")
}
if (selectedImage === "") {
selectedImageValidationError = qsTr("You need to select an image")
}
if (colorPicker.text === "") {
colorValidationError = qsTr("You need to enter a color")
} else if (!Utils.isHexColor(colorPicker.text)) {
colorValidationError = qsTr("This field needs to be an hexadecimal color (eg: #4360DF)")
}
return !nameValidationError && !descriptionTextArea.validationError && !colorValidationError
}
title: qsTr("New community")
ScrollView {
property ScrollBar vScrollBar: ScrollBar.vertical
id: scrollView
anchors.fill: parent
rightPadding: Style.current.padding
anchors.rightMargin: - Style.current.halfPadding
contentHeight: content.height
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AlwaysOn
clip: true
function scrollBackUp() {
vScrollBar.setPosition(0)
}
Item {
id: content
height: childrenRect.height
width: parent.width
Input {
id: nameInput
label: qsTr("Name your community")
placeholderText: qsTr("A catchy name")
validationError: popup.nameValidationError
}
StyledTextArea {
id: descriptionTextArea
label: qsTr("Give it a short description")
placeholderText: qsTr("What your community is about")
validationError: descriptionTextArea.text.length > maxDescChars ? qsTr("The description cannot exceed 140 characters") : ""
anchors.top: nameInput.bottom
anchors.topMargin: Style.current.bigPadding
customHeight: 88
}
StyledText {
id: charLimit
text: `${descriptionTextArea.text.length}/${maxDescChars}`
anchors.top: descriptionTextArea.bottom
anchors.topMargin: !descriptionTextArea.validationError ? 5 : - Style.current.smallPadding
anchors.right: descriptionTextArea.right
font.pixelSize: 12
color: !descriptionTextArea.validationError ? Style.current.textColor : Style.current.danger
}
StyledText {
id: thumbnailText
text: qsTr("Thumbnail image")
anchors.top: descriptionTextArea.bottom
anchors.topMargin: Style.current.smallPadding
font.pixelSize: 15
color: Style.current.secondaryText
}
Rectangle {
id: addImageButton
color: imagePreview.visible ? "transparent" : Style.current.inputBackground
width: 128
height: width
radius: width / 2
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: thumbnailText.bottom
anchors.topMargin: Style.current.padding
FileDialog {
id: imageDialog
//% "Please choose an image"
title: qsTrId("please-choose-an-image")
folder: shortcuts.pictures
nameFilters: [
//% "Image files (*.jpg *.jpeg *.png)"
qsTrId("image-files----jpg---jpeg---png-")
]
onAccepted: {
selectedImage = imageDialog.fileUrls[0]
}
}
Image {
id: imagePreview
visible: !!popup.selectedImage
source: popup.selectedImage
fillMode: Image.PreserveAspectCrop
width: parent.width
height: parent.height
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
anchors.centerIn: parent
width: imagePreview.width
height: imagePreview.height
radius: imagePreview.width / 2
}
}
}
Item {
id: addImageCenter
visible: !imagePreview.visible
width: uploadText.width
height: childrenRect.height
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
SVGImage {
id: imageImg
source: "../../../img/images_icon.svg"
width: 20
height: 18
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
id: uploadText
text: qsTr("Upload")
anchors.top: imageImg.bottom
anchors.topMargin: 5
font.pixelSize: 15
color: Style.current.secondaryText
}
}
Rectangle {
color: Style.current.primary
width: 40
height: width
radius: width / 2
anchors.top: parent.top
anchors.right: parent.right
anchors.rightMargin: Style.current.halfPadding
SVGImage {
source: "../../../img/plusSign.svg"
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
width: 13
height: 13
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: imageDialog.open()
}
}
Input {
id: colorPicker
label: qsTr("Community color")
placeholderText: qsTr("Pick a color")
anchors.top: addImageButton.bottom
anchors.topMargin: Style.current.smallPadding
validationError: popup.colorValidationError
StatusIconButton {
icon.name: "caret"
iconRotation: -90
iconColor: Style.current.textColor
icon.width: 13
icon.height: 7
anchors.right: parent.right
anchors.rightMargin: Style.current.smallPadding
anchors.top: parent.top
anchors.topMargin: colorPicker.textField.height / 2 - height / 2 + Style.current.bigPadding
onClicked: colorDialog.open()
}
ColorDialog {
id: colorDialog
title: qsTr("Please choose a color")
onAccepted: {
colorPicker.text = colorDialog.color
}
}
}
Separator {
id: separator1
anchors.top: colorPicker.bottom
anchors.topMargin: Style.current.bigPadding
}
Item {
id: privateSwitcher
height: privateSwitch.height
width: parent.width
anchors.top: separator1.bottom
anchors.topMargin: Style.current.smallPadding * 2
StyledText {
text: qsTr("Private community")
anchors.verticalCenter: parent.verticalCenter
}
StatusSwitch {
id: privateSwitch
anchors.right: parent.right
}
}
StyledText {
id: privateExplanation
anchors.top: privateSwitcher.bottom
wrapMode: Text.WordWrap
anchors.topMargin: Style.current.smallPadding * 2
width: parent.width
text: privateSwitch.checked ?
qsTr("Only members with an invite link will be able to join your community. Private communities are not listed inside Status") :
qsTr("Your community will be public for anyone to join. Public communities are listed inside Status for easy discovery")
}
}
}
footer: StatusButton {
text: qsTr("Create")
anchors.right: parent.right
onClicked: {
if (!validate()) {
scrollView.scrollBackUp()
return
}
const error = chatsModel.createCommunity(Utils.filterXSS(nameInput.text),
Utils.filterXSS(descriptionTextArea.text),
colorPicker.text,
popup.selectedImage)
if (error) {
creatingError.text = error
return creatingError.open()
}
// TODO Open the community once we have designs for it
popup.close()
}
MessageDialog {
id: creatingError
title: qsTr("Error creating the community")
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok
}
}
}

View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.875 15C1.875 7.75126 7.75126 1.875 15 1.875H15.8333C17.099 1.875 18.125 2.90101 18.125 4.16667V10.8333C18.125 12.099 17.099 13.125 15.8333 13.125H15C13.9645 13.125 13.125 13.9645 13.125 15V15.8333C13.125 17.099 12.099 18.125 10.8333 18.125H4.16667C2.90101 18.125 1.875 17.099 1.875 15.8333V15ZM15 3.125C8.44162 3.125 3.125 8.44162 3.125 15V15.8333C3.125 16.4086 3.59137 16.875 4.16667 16.875H4.79167C5.02179 16.875 5.20833 16.6885 5.20833 16.4583V15C5.20833 9.59221 9.59221 5.20833 15 5.20833H16.4583C16.6885 5.20833 16.875 5.02179 16.875 4.79167V4.16667C16.875 3.59137 16.4086 3.125 15.8333 3.125H15ZM8.54167 16.4583C8.54167 16.6885 8.35512 16.875 8.125 16.875H6.875C6.64488 16.875 6.45833 16.6885 6.45833 16.4583V15C6.45833 10.2826 10.2826 6.45833 15 6.45833H16.4583C16.6885 6.45833 16.875 6.64488 16.875 6.875V8.125C16.875 8.35512 16.6885 8.54167 16.4583 8.54167H15C11.4332 8.54167 8.54167 11.4332 8.54167 15V16.4583ZM9.79167 16.4583C9.79167 16.6885 9.97821 16.875 10.2083 16.875H10.8333C11.4086 16.875 11.875 16.4086 11.875 15.8333V15C11.875 13.2741 13.2741 11.875 15 11.875H15.8333C16.4086 11.875 16.875 11.4086 16.875 10.8333V10.2083C16.875 9.97821 16.6885 9.79167 16.4583 9.79167H15C12.1235 9.79167 9.79167 12.1235 9.79167 15V16.4583Z" fill="#4360DF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

4
ui/app/img/member.svg Normal file
View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.00008 7.5C9.24272 7.5 10.2501 6.49264 10.2501 5.25C10.2501 4.00736 9.24272 3 8.00008 3C6.75744 3 5.75008 4.00736 5.75008 5.25C5.75008 6.49264 6.75744 7.5 8.00008 7.5Z" fill="black"/>
<path d="M4.12351 12.0101C4.56402 10.2798 6.1326 9 8.00008 9C9.86756 9 11.4361 10.2798 11.8767 12.0101C12.0129 12.5453 11.5524 13 11.0001 13H5.00008C4.4478 13 3.98725 12.5453 4.12351 12.0101Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 506 B

View File

@ -7,6 +7,7 @@ QtObject {
readonly property int chatTypePublic: 2
readonly property int chatTypePrivateGroupChat: 3
readonly property int chatTypeStatusUpdate: 4
readonly property int communityPublicChat: 6
readonly property int fetchRangeLast24Hours: 86400
readonly property int fetchRangeLast2Days: 172800

View File

@ -183,6 +183,10 @@ QtObject {
return (/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/.test(text))
}
function isHexColor(c) {
return (/^#([0-9A-F]{6}|[0-9A-F]{3})$/i.test(c))
}
function isSpace(c) {
return (/( |\t|\n|\r)/.test(c))
}

View File

@ -166,9 +166,17 @@ DISTFILES += \
app/AppLayouts/Chat/ChatColumn/MessageComponents/UserImage.qml \
app/AppLayouts/Chat/ChatColumn/MessageComponents/UsernameLabel.qml \
app/AppLayouts/Chat/ChatColumn/MessageComponents/qmldir \
app/AppLayouts/Chat/CommunityColumn.qml \
app/AppLayouts/Chat/ContactsColumn/AddChat.qml \
app/AppLayouts/Chat/ContactsColumn/ClosedEmptyView.qml \
app/AppLayouts/Chat/components/ChooseBrowserPopup.qml \
app/AppLayouts/Chat/ContactsColumn/CommunityButton.qml \
app/AppLayouts/Chat/ContactsColumn/CommunityComponents/CreateChannelPopup.qml \
app/AppLayouts/Chat/ContactsColumn/CommunityList.qml \
app/AppLayouts/Chat/ContactsColumn/CommunityWelcomeBanner.qml \
app/AppLayouts/Chat/components/CommunitiesPopup.qml \
app/AppLayouts/Chat/components/CommunityDetailPopup.qml \
app/AppLayouts/Chat/components/CreateCommunityPopup.qml \
app/AppLayouts/Chat/components/EmojiCategoryButton.qml \
app/AppLayouts/Chat/components/EmojiPopup.qml \
app/AppLayouts/Chat/components/EmojiReaction.qml \

View File

@ -50,10 +50,10 @@ Item {
id: textArea
text: ""
font.pixelSize: 15
wrapMode: Text.WordWrap
wrapMode: Text.WrapAnywhere
placeholderText: inputBox.placeholderText
anchors.rightMargin: Style.current.padding
anchors.leftMargin: inputBox.hasIcon ? 36 : Style.current.padding
anchors.rightMargin: Style.current.smallPadding
anchors.leftMargin: inputBox.hasIcon ? 36 : Style.current.smallPadding
anchors.bottomMargin: Style.current.smallPadding
anchors.topMargin: Style.current.smallPadding
anchors.fill: parent

View File

@ -14,7 +14,7 @@ Rectangle {
color: {
const color = chatsModel.getChannelColor(root.chatName.startsWith("#") ? root.chatName.substr(1) : root.chatName)
if (!color) {
return Style.current.transparent
return Style.current.orange
}
return color
}

2
vendor/status-go vendored

@ -1 +1 @@
Subproject commit 149877a939656f3780fb73f5add26e5e205cf26f
Subproject commit 7387049d4b524d3371966fa07ccba3e08b6c652a