wip community
This commit is contained in:
parent
6157744d59
commit
ce3252fb8f
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -12,6 +12,7 @@ type ContentType* {.pure.} = enum
|
|||
Group = 6,
|
||||
Image = 7,
|
||||
Audio = 8
|
||||
Community = 9
|
||||
|
||||
type TextItem* = object
|
||||
textType*: string
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
@ -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){
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
##^##*/
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
##^##*/
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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}
|
||||
}
|
||||
##^##*/
|
|
@ -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')
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 |
|
@ -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 |
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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 \
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 149877a939656f3780fb73f5add26e5e205cf26f
|
||||
Subproject commit 7387049d4b524d3371966fa07ccba3e08b6c652a
|
Loading…
Reference in New Issue