core, ui: terminal chat layout for LLM and service bot chats (#7811)

* plan: UI improvements to improve LLM usage via SimpleX

* core, ui: types for chat layout

* core, ui: settings for alternative terminal-like chat layout for managing LLMs or other service bots

* plan: message priority

* fixes

* query plan

* update plans

* remove silent messages plan

* rename migrations

* implement layout

* amend

* fix

* fix

* fix

* improve

* time on the right, sender name in blue

* no avatar in direct chats

* better previews

* update, fixes

* update

* time gap

* changes

* ui: terminal layout line heights, gaps and time placement

* ui: simplify terminal meta layout, correct its docs and tests

* ui: right-to-left emoji edge in terminal rows, time placement tests

* android, desktop: raise Kotlin compiler memory to 4 GB

* ui: terminal captions as follow-up text, classic event separators

* ui: terminal media edge and overlay time, plan decisions

* ui: simplify terminal row separation, correct doc line references

* ui: keep the media time inside terminal rows, restore caption conditions

* ui: terminal heading capitals at large font scales, idle live message time

* ui: rename the classic voice-with-text flag, correct the meta copy docs

* ui: smaller terminal layout diff, sender gap and meta names

* ui: terminal row as tall as the media time

* docs: remove the chat layout section from the bots readme

* docs: shorten the terminal layout section of the chat view

* core: address settings API as on master, /auto_accept reads the stored layout

* ui: smaller terminal headings

* ui: classic sender rows and avatar rule as on master

* ui: classic reply quote text colour as on master

* ios: keep the chat layout setting with other UI settings

* ui: go-to button at the end of terminal rows, no gutter in direct chats

* ui: forwarded arrow on the header line of terminal rows, above the time

* ui: forwarded arrow centred above the time line of terminal rows

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
Evgeny @ SimpleX ChatandEvgeny Poberezkin authored and GitHub committed 2026-10-25 18:03:11 +00:00
1 parent 4f81c6f632
commit 7810fc4752
103 files changed
+5098 -901

No files matched your search

+11 -5
View File
@@ -58,7 +58,8 @@ data AppSettings = AppSettings
uiCurrentThemeIds :: Maybe (Map ThemeColorScheme Text),
uiThemes :: Maybe [UITheme],
oneHandUI :: Maybe Bool,
chatBottomBar :: Maybe Bool
chatBottomBar :: Maybe Bool,
uiChatLayout :: Maybe ChatLayout
}
deriving (Show)
@@ -109,7 +110,8 @@ defaultAppSettings =
uiCurrentThemeIds = Nothing,
uiThemes = Nothing,
oneHandUI = Just True,
chatBottomBar = Just True
chatBottomBar = Just True,
uiChatLayout = Just CLClassic
}
defaultParseAppSettings :: AppSettings
@@ -147,7 +149,8 @@ defaultParseAppSettings =
uiCurrentThemeIds = Nothing,
uiThemes = Nothing,
oneHandUI = Nothing,
chatBottomBar = Nothing
chatBottomBar = Nothing,
uiChatLayout = Nothing
}
combineAppSettings :: AppSettings -> AppSettings -> AppSettings
@@ -185,7 +188,8 @@ combineAppSettings platformDefaults storedSettings =
uiCurrentThemeIds = p uiCurrentThemeIds,
uiThemes = p uiThemes,
oneHandUI = p oneHandUI,
chatBottomBar = p chatBottomBar
chatBottomBar = p chatBottomBar,
uiChatLayout = p uiChatLayout
}
where
p :: (AppSettings -> Maybe a) -> Maybe a
@@ -240,6 +244,7 @@ instance FromJSON AppSettings where
uiThemes <- p "uiThemes"
oneHandUI <- p "oneHandUI"
chatBottomBar <- p "chatBottomBar"
uiChatLayout <- p "uiChatLayout"
pure
AppSettings
{ appPlatform,
@@ -274,7 +279,8 @@ instance FromJSON AppSettings where
uiCurrentThemeIds,
uiThemes,
oneHandUI,
chatBottomBar
chatBottomBar,
uiChatLayout
}
where
p key = v .:? key <|> pure Nothing
+2 -2
View File
@@ -67,8 +67,8 @@ initializeBotAddress' logAddress pqRatchet_ doAutoAccept cc = do
putStrLn $ "Bot's contact address is: " <> B.unpack (maybe (strEncode uri) strEncode shortUri)
when (isJust shortUri) $ putStrLn $ "Full contact address for old clients: " <> B.unpack (strEncode uri)
let aa = if doAutoAccept then Just AutoAccept {acceptIncognito = False} else Nothing
settings = AddressSettings {businessAddress = False, autoAccept = aa, autoReply = Nothing}
void $ sendChatCmd cc $ SetAddressSettings Nothing settings
settings = AddressSettings {businessAddress = False, autoAccept = aa, chatLayout = Nothing, autoReply = Nothing}
void $ sendChatCmd cc $ SetAddressSettings Nothing settings Nothing
sendMessage :: ChatController -> Contact -> Text -> IO ()
sendMessage cc ct = sendComposedMessage cc ct Nothing . MCText
+2 -1
View File
@@ -593,7 +593,7 @@ data ChatCommand
| APISetProfileAddress {userId :: UserId, enable :: Bool}
| SetProfileAddress Bool
| APISetAddressSettings {userId :: UserId, pqRatchet :: Maybe Bool, settings :: AddressSettings}
| SetAddressSettings {pqRatchet :: Maybe Bool, settings :: AddressSettings}
| SetAddressSettings {pqRatchet :: Maybe Bool, settings :: AddressSettings, chatLayout :: Maybe (Maybe ChatLayout)}
| AcceptContact IncognitoEnabled ContactName
| RejectContact ContactName Bool
| ForwardMessage {toChatName :: ChatName, fromContactName :: ContactName, forwardedMsg :: Text}
@@ -679,6 +679,7 @@ data ChatCommand
| SetGroupFeature AGroupFeatureNoRole GroupName GroupFeatureEnabled
| SetGroupFeatureRole AGroupFeatureRole GroupName GroupFeatureEnabled (Maybe GroupMemberRole)
| SetGroupMemberAdmissionReview GroupName (Maybe MemberCriteria)
| SetGroupChatLayout GroupName (Maybe ChatLayout)
| SetUserTimedMessages Bool -- UserId (not used in UI)
| SetContactTimedMessages ContactName (Maybe TimedMessagesEnabled)
| SetGroupTimedMessages GroupName (Maybe Int)
+23 -12
View File
@@ -100,6 +100,7 @@ import Simplex.Chat.Store.Shared
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.UITheme (ChatLayout (..))
import Simplex.Chat.Util (liftIOEither, zipWith3')
import qualified Simplex.Chat.Util as U
import Simplex.Chat.Web (webPreviewWorker)
@@ -2196,14 +2197,14 @@ processChatCommand cxt nm = \case
pure $ CRConnectionPlan user ccLink planSimplexName otherSimplexName plan
APIConnectPlan _ Nothing _ _ -> throwChatError CEInvalidConnReq
APIPrepareContact userId accLink verifiedDomain contactSLinkData -> withUserId userId $ \user -> do
let ContactShortLinkData {profile, message, business} = contactSLinkData
let ContactShortLinkData {profile, message, business, chatLayout} = contactSLinkData
welcomeSharedMsgId <- forM message $ \_ -> getSharedMsgId
case accLink of
ACCL SCMContact ccLink
| business -> do
let Profile {preferences} = profile
groupPreferences = maybe defaultBusinessGroupPrefs businessGroupPrefs preferences
groupProfile = businessGroupProfile profile groupPreferences
groupProfile = businessGroupProfile profile groupPreferences chatLayout
gVar <- asks random
(gInfo, hostMember_) <- withStore $ \db -> createPreparedGroup db gVar cxt user groupProfile True ccLink welcomeSharedMsgId False GRMember Nothing verifiedDomain
hostMember <- maybe (throwCmdError "no host member") pure hostMember_
@@ -2217,8 +2218,9 @@ processChatCommand cxt nm = \case
Just (AChatItem SCTGroup dir _ ci) -> Chat cInfo [CChatItem dir ci] emptyChatStats {unreadCount = 1, minUnreadItemId = chatItemId' ci}
_ -> Chat cInfo [] emptyChatStats
pure $ CRNewPreparedChat user $ AChat SCTGroup chat
ACCL _ (CCLink cReq sLnk) -> do
ct <- withStore $ \db -> createPreparedContact db cxt user (linkPresHeader <$> sLnk) profile accLink welcomeSharedMsgId (True <$ verifiedDomain)
ACCL m (CCLink cReq sLnk) -> do
let ctLayout = case m of SCMContact -> chatLayout; SCMInvitation -> Nothing
ct <- withStore $ \db -> createPreparedContact db cxt user (linkPresHeader <$> sLnk) profile accLink welcomeSharedMsgId ctLayout (True <$ verifiedDomain)
void $ createChatItem user (CDDirectSnd ct) False CIChatBanner Nothing Nothing (Just epochStart)
let cd = CDDirectRcv ct
createItem sharedMsgId content = createChatItem user cd False content sharedMsgId Nothing Nothing
@@ -2540,8 +2542,9 @@ processChatCommand cxt nm = \case
ucl'' <- if shortLinkDataSet then setMyAddressData False pqInitKeys user ucl' else pure ucl'
withFastStore' $ \db -> updateUserAddressSettings db userContactLinkId settings
pure $ CRUserContactLinkUpdated user ucl''
SetAddressSettings pqRatchet_ settings -> withUser $ \User {userId} ->
processChatCommand cxt nm $ APISetAddressSettings userId pqRatchet_ settings
SetAddressSettings pqRatchet_ settings chatLayout_ -> withUser $ \user@User {userId} -> do
UserContactLink {addressSettings = AddressSettings {chatLayout}} <- withFastStore (`getUserAddress` user)
processChatCommand cxt nm $ APISetAddressSettings userId pqRatchet_ (settings :: AddressSettings) {chatLayout = fromMaybe chatLayout chatLayout_}
AcceptContact incognito cName -> withUser $ \User {userId} -> do
connReqId <- withFastStore $ \db -> getContactRequestIdByName db userId cName
processChatCommand cxt nm $ APIAcceptContact incognito connReqId
@@ -3676,6 +3679,8 @@ processChatCommand cxt nm = \case
case memberAdmission of
Nothing -> p {memberAdmission = Just (emptyGroupMemberAdmission :: GroupMemberAdmission) {review = reviewAdmissionApplication}}
Just ma -> p {memberAdmission = Just (ma :: GroupMemberAdmission) {review = reviewAdmissionApplication}}
SetGroupChatLayout gName chatLayout ->
updateGroupProfileByName gName $ \p -> p {chatLayout}
SetUserTimedMessages onOff -> withUserProfileLock "updateProfile" $ withUser $ \user@User {profile} -> do
let allowed = if onOff then FAYes else FANo
pref = TimedMessagesPreference allowed Nothing
@@ -4836,7 +4841,8 @@ processChatCommand cxt nm = \case
contactShortLinkData p settings =
let msg = autoReply =<< settings
business = maybe False businessAddress settings
contactData = ContactShortLinkData p msg business Nothing
layout = settings >>= \AddressSettings {chatLayout} -> chatLayout
contactData = ContactShortLinkData p msg business layout Nothing
in encodeShortLinkData contactData
relayShortLinkData :: Profile -> UserLinkData
relayShortLinkData Profile {displayName, fullName, shortDescr, image} =
@@ -6617,7 +6623,7 @@ chatCommandP =
"/_profile_address " *> (APISetProfileAddress <$> A.decimal <* A.space <*> onOffP),
("/profile_address " <|> "/pa ") *> (SetProfileAddress <$> onOffP),
"/_address_settings " *> (APISetAddressSettings <$> A.decimal <*> optional (" pq_ratchet=" *> onOffP) <* A.space <*> jsonP),
"/auto_accept" *> (SetAddressSettings <$> optional (" pq_ratchet=" *> onOffP) <* A.space <*> autoAcceptP),
"/auto_accept" *> (uncurry . SetAddressSettings <$> optional (" pq_ratchet=" *> onOffP) <* A.space <*> autoAcceptP),
("/accept" <|> "/ac") *> (AcceptContact <$> incognitoP <* A.space <* char_ '@' <*> displayNameP),
("/reject " <|> "/rc ") *> char_ '@' *> (RejectContact <$> displayNameP <*> (" notify" $> True <|> pure False)),
("/markdown" <|> "/m") $> ChatHelp HSMarkdown,
@@ -6653,6 +6659,7 @@ chatCommandP =
"/set support #" *> (SetGroupFeature (AGFNR SGFSupport) <$> displayNameP <*> (A.space *> strP)),
"/set links #" *> (SetGroupFeatureRole (AGFR SGFSimplexLinks) <$> displayNameP <*> _strP <*> optional memberRole),
"/set admission review #" *> (SetGroupMemberAdmissionReview <$> displayNameP <*> (A.space *> memberCriteriaP)),
"/set layout #" *> (SetGroupChatLayout <$> displayNameP <* A.space <*> chatLayoutP_),
("/incognito" <* optional (A.space *> onOffP)) $> ChatHelp HSIncognito,
"/set device name " *> (SetLocalDeviceName <$> textP),
"/list remote hosts" $> ListRemoteHosts,
@@ -6796,12 +6803,14 @@ chatCommandP =
{ directMessages = Just DirectMessagesGroupPreference {enable = FEOn, role = Nothing},
history = Just HistoryGroupPreference {enable = FEOn}
}
pure GroupProfile {displayName = gName, fullName = "", shortDescr, description = Nothing, image = Nothing, publicGroup = Nothing, groupPreferences, memberAdmission = Nothing}
pure GroupProfile {displayName = gName, fullName = "", shortDescr, description = Nothing, image = Nothing, publicGroup = Nothing, groupPreferences, memberAdmission = Nothing, chatLayout = Nothing}
channelProfile = do
p@GroupProfile {groupPreferences = prefs_} <- groupProfile
let prefs = (fromMaybe emptyGroupPrefs prefs_) {support = Just SupportGroupPreference {enable = FEOff}} :: GroupPreferences
pure p {groupPreferences = Just prefs}
memberCriteriaP = ("all" $> Just MCAll) <|> ("off" $> Nothing)
chatLayoutP = ("classic" $> CLClassic) <|> ("terminal" $> CLTerminal)
chatLayoutP_ = ("default" $> Nothing) <|> (Just <$> chatLayoutP)
shortDescrP = do
descr <- A.takeWhile1 isSpace *> (T.dropWhileEnd isSpace <$> textP) <|> pure ""
pure $ if T.null descr then Nothing else Just $ T.take 160 descr
@@ -6922,10 +6931,12 @@ chatCommandP =
dbEncryptionConfig currentKey newKey = DBEncryptionConfig {currentKey, newKey, keepKey = Just False}
#endif
-- TODO [short links] parser for address settings
autoAcceptP = ifM onOffP (businessAA <|> addressAA) (pure $ AddressSettings False Nothing Nothing)
autoAcceptP = ifM onOffP (businessAA <|> addressAA) (toSettings False Nothing <$> layoutP <*> pure Nothing)
where
addressAA = AddressSettings False <$> (Just . AutoAccept <$> (" incognito=" *> onOffP <|> pure False)) <*> autoReply
businessAA = " business" *> (AddressSettings True (Just $ AutoAccept False) <$> autoReply)
addressAA = toSettings False <$> (Just . AutoAccept <$> (" incognito=" *> onOffP <|> pure False)) <*> layoutP <*> autoReply
businessAA = " business" *> (toSettings True (Just $ AutoAccept False) <$> layoutP <*> autoReply)
toSettings business autoAccept chatLayout_ autoReply_ = (AddressSettings business autoAccept Nothing autoReply_, chatLayout_)
layoutP = optional (" layout=" *> chatLayoutP_)
autoReply = optional (A.space *> msgContentP)
rcCtrlAddressP = RCCtrlAddress <$> ("addr=" *> strP) <*> (" iface=" *> (quotedP <|> text1P))
quotedP = safeDecodeUtf8 <$> (A.char '"' *> A.takeTill (== '"') <* A.char '"')
+7 -5
View File
@@ -78,6 +78,7 @@ import Simplex.Chat.Types
import Simplex.Chat.Types.MemberRelations
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.UITheme (ChatLayout)
import Simplex.Chat.Util (encryptFile, shuffle)
import Simplex.FileTransfer.Description (FileDescriptionURI (..), ValidFileDescription)
import qualified Simplex.FileTransfer.Description as FD
@@ -1125,10 +1126,11 @@ acceptGroupJoinSendRejectAsync
agentAcceptContactAsync cmdId acId False cReqInvId msg PQSupportOff subMode
pure m
acceptBusinessJoinRequestAsync :: User -> Int64 -> GroupInfoKeys -> GroupMember -> UserContactRequest -> CM (GroupInfo, GroupMember)
acceptBusinessJoinRequestAsync :: User -> Int64 -> Maybe ChatLayout -> GroupInfoKeys -> GroupMember -> UserContactRequest -> CM (GroupInfo, GroupMember)
acceptBusinessJoinRequestAsync
user
uclId
chatLayout
(GIK gInfo@GroupInfo {membership = GroupMember {memberRole = userRole, memberId = userMemberId}} gks)
clientMember@GroupMember {groupMemberId, memberId}
UserContactRequest {agentInvitationId = AgentInvId cReqInvId, cReqChatVRange, xContactId} = do
@@ -1143,7 +1145,7 @@ acceptBusinessJoinRequestAsync
fromMemberName = displayName,
fromMemberKey = Just $ groupMemberKey gks,
invitedMember = MemberIdRole memberId GRMember,
groupProfile = businessGroupProfile userProfile groupPreferences,
groupProfile = businessGroupProfile userProfile groupPreferences chatLayout,
accepted = Just GAAccepted,
-- This refers to the "title member" that defines the group name and profile.
-- This coincides with fromMember to be current user when accepting the connecting user,
@@ -1213,9 +1215,9 @@ rejectRelayInvitationAsync user uclId cxt groupRelayInv presHeader_ invId reqCha
createJoiningMemberConnection db user uclId (cmdId, acId) chatV reqChatVRange groupMemberId subMode
agentAcceptContactAsync cmdId acId False invId msg PQSupportOff subMode
businessGroupProfile :: Profile -> GroupPreferences -> GroupProfile
businessGroupProfile Profile {displayName, fullName, shortDescr, description, image} groupPreferences =
GroupProfile {displayName, fullName, description, shortDescr, image, publicGroup = Nothing, groupPreferences = Just groupPreferences, memberAdmission = Nothing}
businessGroupProfile :: Profile -> GroupPreferences -> Maybe ChatLayout -> GroupProfile
businessGroupProfile Profile {displayName, fullName, shortDescr, description, image} groupPreferences chatLayout =
GroupProfile {displayName, fullName, description, shortDescr, image, publicGroup = Nothing, groupPreferences = Just groupPreferences, memberAdmission = Nothing, chatLayout}
introduceToModerators :: StoreCxt -> User -> GroupInfoKeys -> GroupMember -> CM ()
introduceToModerators cxt user gInfo@(GIK g@GroupInfo {groupId} _) m@GroupMember {memberRole, memberId} = do
+2 -2
View File
@@ -1497,7 +1497,7 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
-- ##### Contact requests (regular and business contacts) #####
Nothing -> do
let UserContactLink {connLinkContact = CCLink connReq _, shortLinkDataSet, addressSettings} = ucl
AddressSettings {autoAccept} = addressSettings
AddressSettings {autoAccept, chatLayout} = addressSettings
isSimplexTeam = sameConnReqContact connReq adminContactReq
gVar <- asks random
withStore (\db -> createOrUpdateContactRequest db gVar cxt user uclId ucl isSimplexTeam invId chatVRange (Just reqHeader) p xContactId_ welcomeMsgId_ requestMsg_ reqPQSup rejectionSupported) >>= \case
@@ -1540,7 +1540,7 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
ct' <- acceptContactRequestAsync user uclId ct ucr incognitoProfile
toView $ CEvtAcceptingContactRequest user ct'
Just (REBusinessChat g@(GIK gInfo _) clientMember) -> do
(_gInfo', _clientMember') <- acceptBusinessJoinRequestAsync user uclId g clientMember ucr
(_gInfo', _clientMember') <- acceptBusinessJoinRequestAsync user uclId chatLayout g clientMember ucr
let cd = CDGroupRcv gInfo Nothing clientMember
void $ case prevUcr_ of
Just UserContactRequest {requestSharedMsgId = prevSharedMsgId_} ->
+2
View File
@@ -56,6 +56,7 @@ import Simplex.Chat.Options.DB (FromField (..), ToField (..))
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.UITheme (ChatLayout)
import qualified Simplex.FileTransfer.Description as FD
import Simplex.Messaging.Agent.Store.DB (blobFieldDecoder, fromTextField_)
import Simplex.Messaging.Compression (Compressed, compress1, decompress1, decompressedSize)
@@ -1612,6 +1613,7 @@ data ContactShortLinkData = ContactShortLinkData
{ profile :: Profile,
message :: Maybe MsgContent,
business :: Bool,
chatLayout :: Maybe ChatLayout,
-- set by the receiving client for the UI: the link profile's badge, verified and crypto-free.
-- never part of the published link data (the link carries the proof inside profile).
localBadge :: Maybe LocalBadge
+4 -4
View File
@@ -122,7 +122,7 @@ getConnectionEntityKeys db cxt user@User {userId, userContactId} agentConnId = d
c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite,
p.preferences, p.preferences_json, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_full_link_to_connect, c.conn_short_link_to_connect, c.welcome_shared_msg_id, c.request_shared_msg_id, c.contact_request_id, cr2.rejection_supported,
c.contact_group_member_id, c.contact_grp_inv_sent, c.grp_direct_inv_link, c.grp_direct_inv_from_group_id, c.grp_direct_inv_from_group_member_id, c.grp_direct_inv_from_member_conn_id, c.grp_direct_inv_started_connection,
c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl,
c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl, c.chat_layout,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx,
p.contact_domain, p.contact_domain_proof, p.contact_domain_verified
FROM contacts c
@@ -132,7 +132,7 @@ getConnectionEntityKeys db cxt user@User {userId, userContactId} agentConnId = d
|]
(userId, contactId, CSActive)
toContact' :: UTCTime -> Int64 -> Connection -> [ChatTagId] -> ContactRow' -> Contact
toContact' currentTs contactId conn chatTags ((profileId, localDisplayName, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, encodedPrefs, receivedPrefs, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, rejectionSupported_, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL) :. badgeRow :. domainRow) =
toContact' currentTs contactId conn chatTags ((profileId, localDisplayName, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, encodedPrefs, receivedPrefs, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, rejectionSupported_, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL, chatLayout) :. badgeRow :. domainRow) =
let preferences = chatPrefsFromRow encodedPrefs receivedPrefs
profile = LocalProfile {profileId, displayName, fullName, shortDescr, description, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, localBadge = rowToBadge currentTs badgeRow, preferences, localAlias}
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite}
@@ -141,7 +141,7 @@ getConnectionEntityKeys db cxt user@User {userId, userContactId} agentConnId = d
preparedContact = toPreparedContact preparedContactRow
contactRequest = UserContactRequestRef <$> contactRequestId <*> (unBI <$> rejectionSupported_)
groupDirectInv = toGroupDirectInvitation groupDirectInvRow
in Contact {contactId, localDisplayName, profile, activeConn, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, preparedContact, contactRequestId, contactRequest, contactGroupMemberId, contactGrpInvSent, groupDirectInv, chatTags, chatItemTTL, uiThemes, chatDeleted, customData}
in Contact {contactId, localDisplayName, profile, activeConn, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, preparedContact, contactRequestId, contactRequest, contactGroupMemberId, contactGrpInvSent, groupDirectInv, chatTags, chatItemTTL, uiThemes, chatLayout, chatDeleted, customData}
getGroupAndMember_ :: Int64 -> Connection -> ExceptT StoreError IO ((GroupInfo, GroupKeysRow), GroupMember)
getGroupAndMember_ groupMemberId c = do
currentTs <- liftIO getCurrentTime
@@ -155,7 +155,7 @@ getConnectionEntityKeys db cxt user@User {userId, userContactId} agentConnId = d
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id,
gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.preferences_json, gp.member_admission,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.preferences_json, gp.member_admission, gp.chat_layout,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id,
g.business_chat, g.business_member_id, g.customer_member_id,
+1 -1
View File
@@ -120,7 +120,7 @@ createOrUpdateContactRequest
ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, cp.preferences_json, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, cr2.rejection_supported,
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection,
ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, ct.chat_layout,
cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified,
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
+7 -6
View File
@@ -324,7 +324,7 @@ getContactByConnReqHash db cxt user@User {userId} cReqHash1 cReqHash2 = do
ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, cp.preferences_json, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, cr2.rejection_supported,
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection,
ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, ct.chat_layout,
cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx,
cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified,
-- Connection
@@ -404,12 +404,12 @@ createIncognitoProfile db User {userId} p = do
createdAt <- getCurrentTime
createIncognitoProfile_ db userId createdAt p
createPreparedContact :: DB.Connection -> StoreCxt -> User -> Maybe ProofPresHeader -> Profile -> ACreatedConnLink -> Maybe SharedMsgId -> Maybe Bool -> ExceptT StoreError IO Contact
createPreparedContact db cxt user presHeader_ p connLinkToConnect welcomeSharedMsgId verified_ = do
createPreparedContact :: DB.Connection -> StoreCxt -> User -> Maybe ProofPresHeader -> Profile -> ACreatedConnLink -> Maybe SharedMsgId -> Maybe ChatLayout -> Maybe Bool -> ExceptT StoreError IO Contact
createPreparedContact db cxt user presHeader_ p connLinkToConnect welcomeSharedMsgId chatLayout verified_ = do
currentTs <- liftIO getCurrentTime
let prepared = Just (connLinkToConnect, welcomeSharedMsgId)
ctUserPreferences = newContactUserPrefs user p
ct <- getContact db cxt user =<< createContact_ db cxt user presHeader_ p ctUserPreferences prepared "" currentTs
ct <- getContact db cxt user =<< createContact_ db cxt user presHeader_ p ctUserPreferences prepared chatLayout "" currentTs
liftIO $ maybe (pure ct) (setContactDomainVerified db user ct) verified_
updatePreparedContactUser :: DB.Connection -> StoreCxt -> User -> Contact -> User -> ExceptT StoreError IO Contact
@@ -454,7 +454,7 @@ createDirectContact :: DB.Connection -> StoreCxt -> User -> Connection -> Maybe
createDirectContact db cxt user Connection {connId, localAlias} presHeader_ p = do
currentTs <- liftIO getCurrentTime
let ctUserPreferences = newContactUserPrefs user p
contactId <- createContact_ db cxt user presHeader_ p ctUserPreferences Nothing localAlias currentTs
contactId <- createContact_ db cxt user presHeader_ p ctUserPreferences Nothing Nothing localAlias currentTs
liftIO $ DB.execute db "UPDATE connections SET contact_id = ?, updated_at = ? WHERE connection_id = ?" (contactId, currentTs, connId)
getContact db cxt user contactId
@@ -927,6 +927,7 @@ createContactFromRequest db user@User {userId, profile = LocalProfile {preferenc
chatTags = [],
chatItemTTL = Nothing,
uiThemes = Nothing,
chatLayout = Nothing,
chatDeleted = False,
customData = Nothing
}
@@ -976,7 +977,7 @@ getContact_ db cxt user@User {userId} contactId deleted = do
ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, cp.preferences_json, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, cr2.rejection_supported,
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection,
ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, ct.chat_layout,
cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx,
cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified,
-- Connection
+20 -19
View File
@@ -382,7 +382,7 @@ setGroupLinkShortLink db gLnk@GroupLink {userContactLinkId, connLinkContact = CC
-- | creates completely new group with a single member - the current user
createNewGroup :: DB.Connection -> StoreCxt -> User -> GroupProfile -> Maybe Profile -> MemberId -> GroupKeys -> Maybe Int64 -> ExceptT StoreError IO GroupInfo
createNewGroup db cxt user@User {userId} groupProfile incognitoProfile memberId groupKeys publicMemberCount_ = ExceptT $ do
let GroupProfile {displayName, fullName, shortDescr, description, image, publicGroup, groupPreferences, memberAdmission} = groupProfile
let GroupProfile {displayName, fullName, shortDescr, description, image, publicGroup, groupPreferences, memberAdmission, chatLayout} = groupProfile
(groupType_, groupLink_, publicGroupId_) = case publicGroup of
Just PublicGroupProfile {groupType, groupLink, publicGroupId} -> (Just groupType, Just groupLink, Just publicGroupId)
Nothing -> (Nothing, Nothing, Nothing)
@@ -406,11 +406,11 @@ createNewGroup db cxt user@User {userId} groupProfile incognitoProfile memberId
(display_name, full_name, short_descr, description, image,
group_type, group_link, public_group_id,
group_web_page, group_domain, domain_web_page, allow_embedding, group_domain_proof,
user_id, member_admission, created_at, updated_at, preferences, preferences_json)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
user_id, member_admission, chat_layout, created_at, updated_at, preferences, preferences_json)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|]
((displayName, fullName, shortDescr, description, image, groupType_, groupLink_, publicGroupId_) :. publicGroupAccessRow publicGroup
:. (userId, memberAdmission, currentTs, currentTs) :. prefsToRow groupPreferences)
:. (userId, memberAdmission, chatLayout, currentTs, currentTs) :. prefsToRow groupPreferences)
profileId <- insertedRowId db
DB.execute
db
@@ -480,7 +480,7 @@ createGroupInvitation db cxt user@User {userId} inviter peerChatVRange GroupInvi
DB.query db "SELECT group_id FROM groups WHERE inv_queue_info = ? AND user_id = ? LIMIT 1" (connRequest, userId)
createGroupInvitation_ :: ExceptT StoreError IO (GroupInfoKeys, GroupMemberId)
createGroupInvitation_ = do
let GroupProfile {displayName, fullName, shortDescr, description, image, groupPreferences, memberAdmission} = groupProfile
let GroupProfile {displayName, fullName, shortDescr, description, image, groupPreferences, memberAdmission, chatLayout} = groupProfile
fullGroupPreferences = mergeGroupPreferences groupPreferences
ExceptT $
withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do
@@ -488,8 +488,8 @@ createGroupInvitation db cxt user@User {userId} inviter peerChatVRange GroupInvi
groupId <- liftIO $ do
DB.execute
db
"INSERT INTO group_profiles (display_name, full_name, short_descr, description, image, user_id, member_admission, created_at, updated_at, preferences, preferences_json) VALUES (?,?,?,?,?,?,?,?,?,?,?)"
((displayName, fullName, shortDescr, description, image, userId, memberAdmission, currentTs, currentTs) :. prefsToRow groupPreferences)
"INSERT INTO group_profiles (display_name, full_name, short_descr, description, image, user_id, member_admission, chat_layout, created_at, updated_at, preferences, preferences_json) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)"
((displayName, fullName, shortDescr, description, image, userId, memberAdmission, chatLayout, currentTs, currentTs) :. prefsToRow groupPreferences)
profileId <- insertedRowId db
DB.execute
db
@@ -927,7 +927,7 @@ createGroupViaLink'
createGroup_ :: DB.Connection -> UserId -> GroupProfile -> Maybe (CreatedLinkContact, Maybe SharedMsgId) -> Maybe BusinessChatInfo -> Bool -> Maybe RelayStatus -> Maybe Int64 -> Maybe C.PrivateKeyEd25519 -> UTCTime -> ExceptT StoreError IO (GroupId, Text)
createGroup_ db userId groupProfile prepared business useRelays relayOwnStatus publicMemberCount_ memberPrivKey_ currentTs = ExceptT $ do
let GroupProfile {displayName, fullName, shortDescr, description, image, publicGroup, groupPreferences, memberAdmission} = groupProfile
let GroupProfile {displayName, fullName, shortDescr, description, image, publicGroup, groupPreferences, memberAdmission, chatLayout} = groupProfile
(groupType_, groupLink_, publicGroupId_) = case publicGroup of
Just PublicGroupProfile {groupType, groupLink, publicGroupId} -> (Just groupType, Just groupLink, Just publicGroupId)
Nothing -> (Nothing, Nothing, Nothing)
@@ -940,11 +940,11 @@ createGroup_ db userId groupProfile prepared business useRelays relayOwnStatus p
(display_name, full_name, short_descr, description, image,
group_type, group_link, public_group_id,
group_web_page, group_domain, domain_web_page, allow_embedding, group_domain_proof,
user_id, member_admission, created_at, updated_at, preferences, preferences_json)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
user_id, member_admission, chat_layout, created_at, updated_at, preferences, preferences_json)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|]
((displayName, fullName, shortDescr, description, image, groupType_, groupLink_, publicGroupId_) :. publicGroupAccessRow publicGroup
:. (userId, memberAdmission, currentTs, currentTs) :. prefsToRow groupPreferences)
:. (userId, memberAdmission, chatLayout, currentTs, currentTs) :. prefsToRow groupPreferences)
profileId <- insertedRowId db
DB.execute
db
@@ -1957,7 +1957,8 @@ createRelayRequestGroup db cxt user@User {userId} GroupRelayInvitation {fromMemb
image = Nothing,
publicGroup = Nothing,
groupPreferences = Nothing,
memberAdmission = Nothing
memberAdmission = Nothing,
chatLayout = Nothing
}
(_, memberPrivKey) <- atomically $ C.generateKeyPair (drg cxt)
(groupId, _groupLDN) <- createGroup_ db userId placeholderProfile Nothing Nothing True (Just relayStatus) Nothing (Just memberPrivKey) currentTs
@@ -2726,7 +2727,7 @@ createMemberConnection_ db userId groupMemberId agentConnId chatV peerChatVRange
createConnection_ db userId ConnMember (Just groupMemberId) agentConnId ConnNew chatV peerChatVRange viaContact Nothing Nothing connLevel currentTs subMode PQSupportOff
updateGroupProfile :: DB.Connection -> User -> GroupInfo -> GroupProfile -> ExceptT StoreError IO GroupInfo
updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName, publicGroup = oldPublicGroup}} p'@GroupProfile {displayName = newName, fullName, shortDescr, description, image, publicGroup, groupPreferences, memberAdmission}
updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName, publicGroup = oldPublicGroup}} p'@GroupProfile {displayName = newName, fullName, shortDescr, description, image, publicGroup, groupPreferences, memberAdmission, chatLayout}
| displayName == newName = liftIO $ do
currentTs <- getCurrentTime
updateGroupProfile_ currentTs
@@ -2762,7 +2763,7 @@ updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName,
display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?,
group_type = ?, group_link = ?,
group_web_page = ?, group_domain = CASE WHEN ? THEN ? ELSE group_domain END, domain_web_page = ?, allow_embedding = ?, group_domain_proof = ?,
member_admission = ?, updated_at = ?
member_admission = ?, chat_layout = ?, updated_at = ?
WHERE group_profile_id IN (
SELECT group_profile_id
FROM groups
@@ -2772,7 +2773,7 @@ updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName,
( prefsToRow groupPreferences
:. (newName, fullName, shortDescr, description, image, groupType_, groupLink_)
:. (groupWebPage_, isJust publicGroup, groupDomain_, domainWebPage_, allowEmbedding_, groupDomainProof_)
:. (memberAdmission, currentTs, userId, groupId)
:. (memberAdmission, chatLayout, currentTs, userId, groupId)
)
updateGroup_ ldn currentTs = do
DB.execute
@@ -2836,17 +2837,17 @@ getGroupProfileById db groupId =
SELECT gp.display_name, gp.full_name, gp.short_descr, gp.description, gp.image,
gp.group_type, gp.group_link, gp.public_group_id,
gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof,
gp.preferences, gp.preferences_json, gp.member_admission
gp.preferences, gp.preferences_json, gp.member_admission, gp.chat_layout
FROM group_profiles gp
JOIN groups g ON gp.group_profile_id = g.group_profile_id
WHERE g.group_id = ?
|]
(Only groupId)
where
toGroupProfile ((displayName, fullName, shortDescr, description, image, groupType_, groupLink_, publicGroupId_) :. accessRow :. (encodedPrefs, receivedPrefs, memberAdmission)) =
toGroupProfile ((displayName, fullName, shortDescr, description, image, groupType_, groupLink_, publicGroupId_) :. accessRow :. (encodedPrefs, receivedPrefs, memberAdmission, chatLayout)) =
let groupPreferences = groupPrefsFromRow encodedPrefs receivedPrefs
publicGroupAccess = toPublicGroupAccess accessRow
in GroupProfile {displayName, fullName, shortDescr, description, image, publicGroup = toPublicGroupProfile groupType_ groupLink_ publicGroupId_ publicGroupAccess, groupPreferences, memberAdmission}
in GroupProfile {displayName, fullName, shortDescr, description, image, publicGroup = toPublicGroupProfile groupType_ groupLink_ publicGroupId_ publicGroupAccess, groupPreferences, memberAdmission, chatLayout}
getGroupInfoByUserContactLinkConnReq :: DB.Connection -> StoreCxt -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe GroupInfo)
getGroupInfoByUserContactLinkConnReq db cxt user@User {userId} (cReqSchema1, cReqSchema2) = do
@@ -3312,7 +3313,7 @@ createMemberContact
quotaErrCounter = 0
}
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn
pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, preparedContact = Nothing, contactRequestId = Nothing, contactRequest = Nothing, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, groupDirectInv = Nothing, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, chatDeleted = False, customData = Nothing}
pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, preparedContact = Nothing, contactRequestId = Nothing, contactRequest = Nothing, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, groupDirectInv = Nothing, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, chatLayout = Nothing, chatDeleted = False, customData = Nothing}
getMemberContact :: DB.Connection -> StoreCxt -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, GroupMember, Contact, ConnReqInvitation)
getMemberContact db cxt user contactId = do
@@ -57,6 +57,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20260925_badge_bindings
import Simplex.Chat.Store.Postgres.Migrations.M20261010_invite_member_via_group
import Simplex.Chat.Store.Postgres.Migrations.M20261012_delivery_job_notification
import Simplex.Chat.Store.Postgres.Migrations.M20261016_badge_store_receipts
import Simplex.Chat.Store.Postgres.Migrations.M20261019_chat_layout
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Text, Maybe Text)]
@@ -113,7 +114,8 @@ schemaMigrations =
("20260925_badge_bindings", m20260925_badge_bindings, Just down_m20260925_badge_bindings),
("20261010_invite_member_via_group", m20261010_invite_member_via_group, Just down_m20261010_invite_member_via_group),
("20261012_delivery_job_notification", m20261012_delivery_job_notification, Just down_m20261012_delivery_job_notification),
("20261016_badge_store_receipts", m20261016_badge_store_receipts, Just down_m20261016_badge_store_receipts)
("20261016_badge_store_receipts", m20261016_badge_store_receipts, Just down_m20261016_badge_store_receipts),
("20261019_chat_layout", m20261019_chat_layout, Just down_m20261019_chat_layout)
]
-- | The list of migrations in ascending order by date
@@ -0,0 +1,23 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.Postgres.Migrations.M20261019_chat_layout where
import Data.Text (Text)
import Text.RawString.QQ (r)
m20261019_chat_layout :: Text
m20261019_chat_layout =
[r|
ALTER TABLE contacts ADD COLUMN chat_layout TEXT;
ALTER TABLE group_profiles ADD COLUMN chat_layout TEXT;
ALTER TABLE user_contact_links ADD COLUMN chat_layout TEXT;
|]
down_m20261019_chat_layout :: Text
down_m20261019_chat_layout =
[r|
ALTER TABLE contacts DROP COLUMN chat_layout;
ALTER TABLE group_profiles DROP COLUMN chat_layout;
ALTER TABLE user_contact_links DROP COLUMN chat_layout;
|]
@@ -795,7 +795,8 @@ CREATE TABLE test_chat_schema.contacts (
grp_direct_inv_from_group_id bigint,
grp_direct_inv_from_group_member_id bigint,
grp_direct_inv_from_member_conn_id bigint,
grp_direct_inv_started_connection smallint DEFAULT 0 NOT NULL
grp_direct_inv_started_connection smallint DEFAULT 0 NOT NULL,
chat_layout text
);
@@ -1092,7 +1093,8 @@ CREATE TABLE test_chat_schema.group_profiles (
domain_web_page bigint,
allow_embedding bigint,
group_domain_proof text,
preferences_json text
preferences_json text,
chat_layout text
);
@@ -1727,7 +1729,8 @@ CREATE TABLE test_chat_schema.user_contact_links (
short_link_contact bytea,
short_link_data_set smallint DEFAULT 0 NOT NULL,
short_link_large_data_set smallint DEFAULT 0 NOT NULL,
link_priv_sig_key bytea
link_priv_sig_key bytea,
chat_layout text
);
+9 -8
View File
@@ -512,6 +512,7 @@ data GroupLinkInfo = GroupLinkInfo
data AddressSettings = AddressSettings
{ businessAddress :: Bool, -- possibly, it can be wrapped together with acceptIncognito, or AutoAccept made sum type
autoAccept :: Maybe AutoAccept, -- accept automatically
chatLayout :: Maybe ChatLayout,
autoReply :: Maybe MsgContent -- included in short link information, sent on acceptance in case wasn't shown during connection
}
deriving (Eq, Show)
@@ -527,11 +528,11 @@ $(J.deriveJSON defaultJSON ''AddressSettings)
$(J.deriveJSON defaultJSON ''UserContactLink)
toUserContactLink :: (Int64, ConnReqContact, Maybe ShortLinkContact, BoolInt, BoolInt, BoolInt, BoolInt, BoolInt, Maybe MsgContent) -> UserContactLink
toUserContactLink (userContactLinkId, connReq, shortLink, BI shortLinkDataSet, BI slLargeDataSet, BI businessAddress, BI autoAccept', BI acceptIncognito, autoReply) =
toUserContactLink :: (Int64, ConnReqContact, Maybe ShortLinkContact, BoolInt, BoolInt, BoolInt, BoolInt, BoolInt, Maybe MsgContent, Maybe ChatLayout) -> UserContactLink
toUserContactLink (userContactLinkId, connReq, shortLink, BI shortLinkDataSet, BI slLargeDataSet, BI businessAddress, BI autoAccept', BI acceptIncognito, autoReply, chatLayout) =
UserContactLink userContactLinkId (CCLink connReq shortLink) shortLinkDataSet (BoolDef slLargeDataSet) $
let autoAccept = if autoAccept' then Just AutoAccept {acceptIncognito} else Nothing
in AddressSettings {businessAddress, autoAccept, autoReply}
in AddressSettings {businessAddress, autoAccept, chatLayout, autoReply}
getUserAddress :: DB.Connection -> User -> ExceptT StoreError IO UserContactLink
getUserAddress db User {userId} =
@@ -544,7 +545,7 @@ getUserContactLinkById db userId userContactLinkId =
DB.query
db
[sql|
SELECT user_contact_link_id, conn_req_contact, short_link_contact, short_link_data_set, short_link_large_data_set, business_address, auto_accept, auto_accept_incognito, auto_reply_msg_content, group_id, group_link_member_role
SELECT user_contact_link_id, conn_req_contact, short_link_contact, short_link_data_set, short_link_large_data_set, business_address, auto_accept, auto_accept_incognito, auto_reply_msg_content, chat_layout, group_id, group_link_member_role
FROM user_contact_links
WHERE user_id = ? AND user_contact_link_id = ?
|]
@@ -586,7 +587,7 @@ getUserContactLinkViaTarget db User {userId, profile = LocalProfile {contactDoma
userContactLinkQuery :: Query
userContactLinkQuery =
[sql|
SELECT user_contact_link_id, conn_req_contact, short_link_contact, short_link_data_set, short_link_large_data_set, business_address, auto_accept, auto_accept_incognito, auto_reply_msg_content
SELECT user_contact_link_id, conn_req_contact, short_link_contact, short_link_data_set, short_link_large_data_set, business_address, auto_accept, auto_accept_incognito, auto_reply_msg_content, chat_layout
FROM user_contact_links
|]
@@ -637,15 +638,15 @@ getContactWithoutConnViaShortAddress db cxt user@User {userId} shortLink = do
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getContact db cxt user) ctId_
updateUserAddressSettings :: DB.Connection -> Int64 -> AddressSettings -> IO ()
updateUserAddressSettings db userContactLinkId AddressSettings {businessAddress, autoAccept, autoReply} =
updateUserAddressSettings db userContactLinkId AddressSettings {businessAddress, autoAccept, chatLayout, autoReply} =
DB.execute
db
[sql|
UPDATE user_contact_links
SET auto_accept = ?, auto_accept_incognito = ?, business_address = ?, auto_reply_msg_content = ?
SET auto_accept = ?, auto_accept_incognito = ?, business_address = ?, auto_reply_msg_content = ?, chat_layout = ?
WHERE user_contact_link_id = ?
|]
(autoAcceptValues :. (BI businessAddress, autoReply, userContactLinkId))
(autoAcceptValues :. (BI businessAddress, autoReply, chatLayout, userContactLinkId))
where
autoAcceptValues = case autoAccept of
Just AutoAccept {acceptIncognito} -> (BI True, BI acceptIncognito)
+3 -1
View File
@@ -181,6 +181,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20261010_invite_member_via_group
import Simplex.Chat.Store.SQLite.Migrations.M20261012_delivery_job_notification
import Simplex.Chat.Store.SQLite.Migrations.M20261016_badge_store_receipts
import Simplex.Chat.Store.SQLite.Migrations.M20261018_autoincrement_ids
import Simplex.Chat.Store.SQLite.Migrations.M20261019_chat_layout
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)]
@@ -361,7 +362,8 @@ schemaMigrations =
("20261010_invite_member_via_group", m20261010_invite_member_via_group, Just down_m20261010_invite_member_via_group),
("20261012_delivery_job_notification", m20261012_delivery_job_notification, Just down_m20261012_delivery_job_notification),
("20261016_badge_store_receipts", m20261016_badge_store_receipts, Just down_m20261016_badge_store_receipts),
("20261018_autoincrement_ids", m20261018_autoincrement_ids, Just down_m20261018_autoincrement_ids)
("20261018_autoincrement_ids", m20261018_autoincrement_ids, Just down_m20261018_autoincrement_ids),
("20261019_chat_layout", m20261019_chat_layout, Just down_m20261019_chat_layout)
]
-- | The list of migrations in ascending order by date
@@ -0,0 +1,22 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.SQLite.Migrations.M20261019_chat_layout where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20261019_chat_layout :: Query
m20261019_chat_layout =
[sql|
ALTER TABLE contacts ADD COLUMN chat_layout TEXT;
ALTER TABLE group_profiles ADD COLUMN chat_layout TEXT;
ALTER TABLE user_contact_links ADD COLUMN chat_layout TEXT;
|]
down_m20261019_chat_layout :: Query
down_m20261019_chat_layout =
[sql|
ALTER TABLE contacts DROP COLUMN chat_layout;
ALTER TABLE group_profiles DROP COLUMN chat_layout;
ALTER TABLE user_contact_links DROP COLUMN chat_layout;
|]
@@ -1535,10 +1535,6 @@ Query: UPDATE ratchets SET ratchet_state = ? WHERE conn_id = ?
Plan:
SEARCH ratchets USING PRIMARY KEY (conn_id=?)
Query: UPDATE rcv_file_chunk_replicas SET delay = ?, retries = retries + 1, updated_at = ? WHERE rcv_file_chunk_replica_id = ?
Plan:
SEARCH rcv_file_chunk_replicas USING INTEGER PRIMARY KEY (rowid=?)
Query: UPDATE rcv_file_chunk_replicas SET received = 1, updated_at = ? WHERE rcv_file_chunk_replica_id = ?
Plan:
SEARCH rcv_file_chunk_replicas USING INTEGER PRIMARY KEY (rowid=?)
@@ -130,7 +130,7 @@ Query:
ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, cp.preferences_json, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, cr2.rejection_supported,
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection,
ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, ct.chat_layout,
cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified,
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
@@ -153,7 +153,7 @@ Query:
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id,
gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.preferences_json, gp.member_admission,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.preferences_json, gp.member_admission, gp.chat_layout,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id,
g.business_chat, g.business_member_id, g.customer_member_id,
@@ -546,7 +546,7 @@ Query:
display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?,
group_type = ?, group_link = ?,
group_web_page = ?, group_domain = CASE WHEN ? THEN ? ELSE group_domain END, domain_web_page = ?, allow_embedding = ?, group_domain_proof = ?,
member_admission = ?, updated_at = ?
member_admission = ?, chat_layout = ?, updated_at = ?
WHERE group_profile_id IN (
SELECT group_profile_id
FROM groups
@@ -758,7 +758,7 @@ Query:
c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite,
p.preferences, p.preferences_json, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_full_link_to_connect, c.conn_short_link_to_connect, c.welcome_shared_msg_id, c.request_shared_msg_id, c.contact_request_id, cr2.rejection_supported,
c.contact_group_member_id, c.contact_grp_inv_sent, c.grp_direct_inv_link, c.grp_direct_inv_from_group_id, c.grp_direct_inv_from_group_member_id, c.grp_direct_inv_from_member_conn_id, c.grp_direct_inv_started_connection,
c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl,
c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl, c.chat_layout,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx,
p.contact_domain, p.contact_domain_proof, p.contact_domain_verified
FROM contacts c
@@ -1372,8 +1372,8 @@ Query:
(display_name, full_name, short_descr, description, image,
group_type, group_link, public_group_id,
group_web_page, group_domain, domain_web_page, allow_embedding, group_domain_proof,
user_id, member_admission, created_at, updated_at, preferences, preferences_json)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
user_id, member_admission, chat_layout, created_at, updated_at, preferences, preferences_json)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
Plan:
@@ -1520,7 +1520,7 @@ Query:
ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, cp.preferences_json, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, cr2.rejection_supported,
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection,
ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, ct.chat_layout,
cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx,
cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified,
-- Connection
@@ -2121,7 +2121,7 @@ Query:
ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, cp.preferences_json, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, cr2.rejection_supported,
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection,
ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, ct.chat_layout,
cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx,
cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified,
-- Connection
@@ -4003,7 +4003,7 @@ Query:
SELECT gp.display_name, gp.full_name, gp.short_descr, gp.description, gp.image,
gp.group_type, gp.group_link, gp.public_group_id,
gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof,
gp.preferences, gp.preferences_json, gp.member_admission
gp.preferences, gp.preferences_json, gp.member_admission, gp.chat_layout
FROM group_profiles gp
JOIN groups g ON gp.group_profile_id = g.group_profile_id
WHERE g.group_id = ?
@@ -4317,7 +4317,7 @@ Plan:
SEARCH usage_conditions USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT user_contact_link_id, conn_req_contact, short_link_contact, short_link_data_set, short_link_large_data_set, business_address, auto_accept, auto_accept_incognito, auto_reply_msg_content, group_id, group_link_member_role
SELECT user_contact_link_id, conn_req_contact, short_link_contact, short_link_data_set, short_link_large_data_set, business_address, auto_accept, auto_accept_incognito, auto_reply_msg_content, chat_layout, group_id, group_link_member_role
FROM user_contact_links
WHERE user_id = ? AND user_contact_link_id = ?
@@ -5468,11 +5468,11 @@ Query:
badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?,
contact_domain = ?, contact_domain_proof = ?
WHERE user_id = ? AND contact_profile_id = ?
Plan:
SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?)
Query:
Query:
UPDATE contact_profiles
SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, contact_link = ?, chat_peer_type = ?, preferences = NULL, preferences_json = NULL, updated_at = ?,
badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?,
@@ -5780,7 +5780,7 @@ SEARCH server_operators USING INTEGER PRIMARY KEY (rowid=?)
Query:
UPDATE user_contact_links
SET auto_accept = ?, auto_accept_incognito = ?, business_address = ?, auto_reply_msg_content = ?
SET auto_accept = ?, auto_accept_incognito = ?, business_address = ?, auto_reply_msg_content = ?, chat_layout = ?
WHERE user_contact_link_id = ?
Plan:
@@ -5856,7 +5856,7 @@ Query:
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id,
gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.preferences_json, gp.member_admission,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.preferences_json, gp.member_admission, gp.chat_layout,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id,
g.business_chat, g.business_member_id, g.customer_member_id,
@@ -5894,7 +5894,7 @@ Query:
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id,
gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.preferences_json, gp.member_admission,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.preferences_json, gp.member_admission, gp.chat_layout,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id,
g.business_chat, g.business_member_id, g.customer_member_id,
@@ -5925,7 +5925,7 @@ Query:
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id,
gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.preferences_json, gp.member_admission,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.preferences_json, gp.member_admission, gp.chat_layout,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id,
g.business_chat, g.business_member_id, g.customer_member_id,
@@ -6699,21 +6699,21 @@ Plan:
SCAN usage_conditions
Query:
SELECT user_contact_link_id, conn_req_contact, short_link_contact, short_link_data_set, short_link_large_data_set, business_address, auto_accept, auto_accept_incognito, auto_reply_msg_content
SELECT user_contact_link_id, conn_req_contact, short_link_contact, short_link_data_set, short_link_large_data_set, business_address, auto_accept, auto_accept_incognito, auto_reply_msg_content, chat_layout
FROM user_contact_links
WHERE user_id = ? AND conn_req_contact IN (?,?)
Plan:
SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?)
Query:
SELECT user_contact_link_id, conn_req_contact, short_link_contact, short_link_data_set, short_link_large_data_set, business_address, auto_accept, auto_accept_incognito, auto_reply_msg_content
SELECT user_contact_link_id, conn_req_contact, short_link_contact, short_link_data_set, short_link_large_data_set, business_address, auto_accept, auto_accept_incognito, auto_reply_msg_content, chat_layout
FROM user_contact_links
WHERE user_id = ? AND local_display_name = '' AND group_id IS NULL
Plan:
SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?)
Query:
SELECT user_contact_link_id, conn_req_contact, short_link_contact, short_link_data_set, short_link_large_data_set, business_address, auto_accept, auto_accept_incognito, auto_reply_msg_content
SELECT user_contact_link_id, conn_req_contact, short_link_contact, short_link_data_set, short_link_large_data_set, business_address, auto_accept, auto_accept_incognito, auto_reply_msg_content, chat_layout
FROM user_contact_links
WHERE user_id = ? AND short_link_contact = ?
Plan:
@@ -7375,7 +7375,7 @@ Query: INSERT INTO contacts (contact_profile_id, local_display_name, user_id, is
Plan:
SEARCH users USING COVERING INDEX sqlite_autoindex_users_1 (contact_id=?)
Query: INSERT INTO contacts (contact_profile_id, user_preferences, local_display_name, user_id, created_at, updated_at, chat_ts, contact_used, conn_full_link_to_connect, conn_short_link_to_connect, welcome_shared_msg_id) VALUES (?,?,?,?,?,?,?,?,?,?,?)
Query: INSERT INTO contacts (contact_profile_id, user_preferences, local_display_name, user_id, created_at, updated_at, chat_ts, contact_used, conn_full_link_to_connect, conn_short_link_to_connect, welcome_shared_msg_id, chat_layout) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
Plan:
SEARCH users USING COVERING INDEX sqlite_autoindex_users_1 (contact_id=?)
@@ -7406,7 +7406,7 @@ Plan:
Query: INSERT INTO files (user_id, group_id, file_name, file_size, chunk_size, file_inline, ci_file_status, protocol, file_type, shared_msg_id, roster_transfer_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
Plan:
Query: INSERT INTO group_profiles (display_name, full_name, short_descr, description, image, user_id, member_admission, created_at, updated_at, preferences, preferences_json) VALUES (?,?,?,?,?,?,?,?,?,?,?)
Query: INSERT INTO group_profiles (display_name, full_name, short_descr, description, image, user_id, member_admission, chat_layout, created_at, updated_at, preferences, preferences_json) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
Plan:
Query: INSERT INTO group_profiles (display_name, full_name, short_descr, image, user_id, created_at, updated_at, preferences, preferences_json) VALUES (?,?,?,?,?,?,?,?,?)
@@ -109,6 +109,7 @@ CREATE TABLE contacts(
grp_direct_inv_from_group_member_id INTEGER REFERENCES group_members(group_member_id) ON DELETE SET NULL,
grp_direct_inv_from_member_conn_id INTEGER REFERENCES connections(connection_id) ON DELETE SET NULL,
grp_direct_inv_started_connection INTEGER NOT NULL DEFAULT 0,
chat_layout TEXT,
FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name)
ON DELETE CASCADE
@@ -149,7 +150,8 @@ CREATE TABLE group_profiles(
domain_web_page INTEGER,
allow_embedding INTEGER,
group_domain_proof TEXT,
preferences_json TEXT
preferences_json TEXT,
chat_layout TEXT
) STRICT;
CREATE TABLE groups(
group_id INTEGER PRIMARY KEY AUTOINCREMENT, -- local group ID
@@ -417,6 +419,7 @@ CREATE TABLE user_contact_links(
short_link_data_set INTEGER NOT NULL DEFAULT 0,
short_link_large_data_set INTEGER NOT NULL DEFAULT 0,
link_priv_sig_key BLOB,
chat_layout TEXT,
UNIQUE(user_id, local_display_name)
) STRICT;
CREATE TABLE contact_requests(
+12 -12
View File
@@ -414,10 +414,10 @@ setCommandConnId db User {userId} cmdId connId = do
createContact :: DB.Connection -> StoreCxt -> User -> Profile -> ExceptT StoreError IO ()
createContact db cxt user profile = do
currentTs <- liftIO getCurrentTime
void $ createContact_ db cxt user Nothing profile emptyChatPrefs Nothing "" currentTs
void $ createContact_ db cxt user Nothing profile emptyChatPrefs Nothing Nothing "" currentTs
createContact_ :: DB.Connection -> StoreCxt -> User -> Maybe ProofPresHeader -> Profile -> Preferences -> Maybe (ACreatedConnLink, Maybe SharedMsgId) -> LocalAlias -> UTCTime -> ExceptT StoreError IO ContactId
createContact_ db cxt User {userId} presHeader_ p@Profile {displayName, fullName, shortDescr, description, image, contactLink, contactDomain, peerType, preferences} ctUserPreferences prepared localAlias currentTs =
createContact_ :: DB.Connection -> StoreCxt -> User -> Maybe ProofPresHeader -> Profile -> Preferences -> Maybe (ACreatedConnLink, Maybe SharedMsgId) -> Maybe ChatLayout -> LocalAlias -> UTCTime -> ExceptT StoreError IO ContactId
createContact_ db cxt User {userId} presHeader_ p@Profile {displayName, fullName, shortDescr, description, image, contactLink, contactDomain, peerType, preferences} ctUserPreferences prepared chatLayout localAlias currentTs =
ExceptT . withLocalDisplayName db userId displayName $ \ldn -> do
(Profile {badge}, badgeVerified) <- profileBadgeVerified presHeader_ (badgeKeys cxt) Nothing p
DB.execute
@@ -427,8 +427,8 @@ createContact_ db cxt User {userId} presHeader_ p@Profile {displayName, fullName
profileId <- insertedRowId db
DB.execute
db
"INSERT INTO contacts (contact_profile_id, user_preferences, local_display_name, user_id, created_at, updated_at, chat_ts, contact_used, conn_full_link_to_connect, conn_short_link_to_connect, welcome_shared_msg_id) VALUES (?,?,?,?,?,?,?,?,?,?,?)"
((profileId, ctUserPreferences, ldn, userId, currentTs, currentTs, currentTs, BI True) :. toPreparedContactRow prepared)
"INSERT INTO contacts (contact_profile_id, user_preferences, local_display_name, user_id, created_at, updated_at, chat_ts, contact_used, conn_full_link_to_connect, conn_short_link_to_connect, welcome_shared_msg_id, chat_layout) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)"
((profileId, ctUserPreferences, ldn, userId, currentTs, currentTs, currentTs, BI True) :. toPreparedContactRow prepared :. Only chatLayout)
contactId <- insertedRowId db
pure $ Right contactId
@@ -490,14 +490,14 @@ type PreparedContactRow = (Maybe AConnectionRequestUri, Maybe AConnShortLink, Ma
type GroupDirectInvitationRow = (Maybe ConnReqInvitation, Maybe GroupId, Maybe GroupMemberId, Maybe Int64, BoolInt)
type ContactRow' = (ProfileId, ContactName, ContactName, Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, BoolInt, ContactStatus) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Text, Maybe Text, Preferences, UTCTime, UTCTime, Maybe UTCTime) :. PreparedContactRow :. (Maybe Int64, Maybe BoolInt, Maybe GroupMemberId, BoolInt) :. GroupDirectInvitationRow :. (Maybe UIThemeEntityOverrides, BoolInt, Maybe CustomData, Maybe Int64) :. BadgeRow :. ContactDomainRow
type ContactRow' = (ProfileId, ContactName, ContactName, Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, BoolInt, ContactStatus) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Text, Maybe Text, Preferences, UTCTime, UTCTime, Maybe UTCTime) :. PreparedContactRow :. (Maybe Int64, Maybe BoolInt, Maybe GroupMemberId, BoolInt) :. GroupDirectInvitationRow :. (Maybe UIThemeEntityOverrides, BoolInt, Maybe CustomData, Maybe Int64, Maybe ChatLayout) :. BadgeRow :. ContactDomainRow
type ContactRow = Only ContactId :. ContactRow'
type ContactDomainRow = (Maybe SimplexDomain, Maybe SimplexDomainProof, Maybe BoolInt)
toContact :: UTCTime -> StoreCxt -> User -> [ChatTagId] -> ContactRow :. MaybeConnectionRow -> Contact
toContact now cxt user chatTags ((Only contactId :. (profileId, localDisplayName, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, encodedPrefs, receivedPrefs, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, rejectionSupported_, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL) :. badgeRow :. domainRow) :. connRow) =
toContact now cxt user chatTags ((Only contactId :. (profileId, localDisplayName, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, encodedPrefs, receivedPrefs, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, rejectionSupported_, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL, chatLayout) :. badgeRow :. domainRow) :. connRow) =
let preferences = chatPrefsFromRow encodedPrefs receivedPrefs
profile = LocalProfile {profileId, displayName, fullName, shortDescr, description, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, localBadge = rowToBadge now badgeRow, preferences, localAlias}
activeConn = toMaybeConnection cxt connRow
@@ -507,7 +507,7 @@ toContact now cxt user chatTags ((Only contactId :. (profileId, localDisplayName
preparedContact = toPreparedContact preparedContactRow
contactRequest = UserContactRequestRef <$> contactRequestId <*> (unBI <$> rejectionSupported_)
groupDirectInv = toGroupDirectInvitation groupDirectInvRow
in Contact {contactId, localDisplayName, profile, activeConn, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, preparedContact, contactRequestId, contactRequest, contactGroupMemberId, contactGrpInvSent, groupDirectInv, chatTags, chatItemTTL, uiThemes, chatDeleted, customData}
in Contact {contactId, localDisplayName, profile, activeConn, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, preparedContact, contactRequestId, contactRequest, contactGroupMemberId, contactGrpInvSent, groupDirectInv, chatTags, chatItemTTL, uiThemes, chatLayout, chatDeleted, customData}
rowToContactDomain :: ContactDomainRow -> Maybe SimplexDomainClaim
rowToContactDomain (domain_, domainProof_, _) = (`SimplexDomainClaim` domainProof_) . StrJSON <$> domain_
@@ -696,7 +696,7 @@ type BusinessChatInfoRow = (Maybe BusinessChatType, Maybe MemberId, Maybe Member
type GroupKeysRow = (Maybe C.PrivateKeyEd25519, Maybe C.PublicKeyEd25519, Maybe C.PrivateKeyEd25519)
type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Text, Maybe Text, Maybe ImageData, Maybe GroupType, Maybe ShortLinkContact, Maybe B64UrlByteString) :. PublicGroupAccessRow :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Text, Maybe Text, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. PreparedGroupRow :. BusinessChatInfoRow :. (BoolInt, Maybe RelayStatus, Maybe UIThemeEntityOverrides, Int64, Maybe Int64, Maybe VersionRoster, Maybe CustomData, Maybe Int64, Int, Maybe ConnReqContact, Maybe BoolInt) :. GroupKeysRow :. GroupMemberRow
type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Text, Maybe Text, Maybe ImageData, Maybe GroupType, Maybe ShortLinkContact, Maybe B64UrlByteString) :. PublicGroupAccessRow :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Text, Maybe Text, Maybe GroupMemberAdmission, Maybe ChatLayout) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. PreparedGroupRow :. BusinessChatInfoRow :. (BoolInt, Maybe RelayStatus, Maybe UIThemeEntityOverrides, Int64, Maybe Int64, Maybe VersionRoster, Maybe CustomData, Maybe Int64, Int, Maybe ConnReqContact, Maybe BoolInt) :. GroupKeysRow :. GroupMemberRow
type PublicGroupAccessRow = (Maybe Text, Maybe SimplexDomain, Maybe BoolInt, Maybe BoolInt, Maybe SimplexDomainProof)
@@ -705,13 +705,13 @@ type GroupMemberRow = (GroupMemberId, GroupId, Int64, MemberId, VersionChat, Ver
type ProfileRow = (ProfileId, ContactName, Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, Maybe Text, Maybe Text) :. BadgeRow :. ContactDomainRow
toGroupInfo :: UTCTime -> StoreCxt -> Int64 -> [ChatTagId] -> GroupInfoRow -> (GroupInfo, GroupKeysRow)
toGroupInfo now cxt userContactId chatTags ((groupId, localDisplayName, displayName, fullName, shortDescr, localAlias, description, image, groupType_, groupLink_, publicGroupId_) :. accessRow :. (enableNtfs_, sendRcpts, BI favorite, encodedPrefs, receivedPrefs, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. preparedGroupRow :. businessRow :. (BI useRelays, relayOwnStatus, uiThemes, currentMembers, publicMemberCount, rosterVersion, customData, chatItemTTL, membersRequireAttention, viaGroupLinkUri, groupDomainVerified) :. groupKeysRow :. userMemberRow) =
toGroupInfo now cxt userContactId chatTags ((groupId, localDisplayName, displayName, fullName, shortDescr, localAlias, description, image, groupType_, groupLink_, publicGroupId_) :. accessRow :. (enableNtfs_, sendRcpts, BI favorite, encodedPrefs, receivedPrefs, memberAdmission, chatLayout) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. preparedGroupRow :. businessRow :. (BI useRelays, relayOwnStatus, uiThemes, currentMembers, publicMemberCount, rosterVersion, customData, chatItemTTL, membersRequireAttention, viaGroupLinkUri, groupDomainVerified) :. groupKeysRow :. userMemberRow) =
let membership = (toGroupMember now userContactId userMemberRow) {memberChatVRange = vr cxt}
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite}
groupPreferences = groupPrefsFromRow encodedPrefs receivedPrefs
fullGroupPreferences = mergeGroupPreferences groupPreferences
publicGroup = toPublicGroupProfile groupType_ groupLink_ publicGroupId_ (toPublicGroupAccess accessRow)
groupProfile = GroupProfile {displayName, fullName, shortDescr, description, image, publicGroup, groupPreferences, memberAdmission}
groupProfile = GroupProfile {displayName, fullName, shortDescr, description, image, publicGroup, groupPreferences, memberAdmission, chatLayout}
businessChat = toBusinessChatInfo (toPublicGroupAccess accessRow >>= groupDomainClaim) businessRow
preparedGroup = toPreparedGroup preparedGroupRow
groupSummary = GroupSummary {currentMembers, publicMemberCount}
@@ -843,7 +843,7 @@ groupInfoQueryFields =
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id,
gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.preferences_json, gp.member_admission,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.preferences_json, gp.member_admission, gp.chat_layout,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id,
g.business_chat, g.business_member_id, g.customer_member_id,
+3 -1
View File
@@ -220,6 +220,7 @@ data Contact = Contact
chatTags :: [ChatTagId],
chatItemTTL :: Maybe Int64,
uiThemes :: Maybe UIThemeEntityOverrides,
chatLayout :: Maybe ChatLayout,
chatDeleted :: Bool,
customData :: Maybe CustomData
}
@@ -919,7 +920,8 @@ data GroupProfile = GroupProfile
image :: Maybe ImageData,
publicGroup :: Maybe PublicGroupProfile,
groupPreferences :: Maybe GroupPreferences,
memberAdmission :: Maybe GroupMemberAdmission
memberAdmission :: Maybe GroupMemberAdmission,
chatLayout :: Maybe ChatLayout
}
deriving (Eq, Show)
+27 -2
View File
@@ -32,7 +32,8 @@ data UIColorMode = UCMLight | UCMDark
data UIThemeEntityOverrides = UIThemeEntityOverrides
{ light :: Maybe UIThemeEntityOverride,
dark :: Maybe UIThemeEntityOverride
dark :: Maybe UIThemeEntityOverride,
chatLayout :: Maybe ChatLayout
}
deriving (Eq, Show)
@@ -43,6 +44,30 @@ data UIThemeEntityOverride = UIThemeEntityOverride
}
deriving (Eq, Show)
data ChatLayout = CLClassic | CLTerminal | CLUnknown Text
deriving (Eq, Show)
instance FromJSON ChatLayout where
parseJSON = textParseJSON "ChatLayout"
instance ToJSON ChatLayout where
toJSON = J.String . textEncode
toEncoding = JE.text . textEncode
instance FromField ChatLayout where fromField = fromTextField_ textDecode
instance ToField ChatLayout where toField = toField . textEncode
instance TextEncoding ChatLayout where
textDecode s = Just $ case s of
"classic" -> CLClassic
"terminal" -> CLTerminal
tag -> CLUnknown tag
textEncode = \case
CLClassic -> "classic"
CLTerminal -> "terminal"
CLUnknown tag -> tag
data DarkColorScheme = DCSDark | DCSBlack | DCSSimplex
deriving (Eq, Ord, Show)
@@ -172,4 +197,4 @@ instance ToField UIThemeEntityOverrides where
toField = toField . encodeJSON
instance FromField UIThemeEntityOverrides where
fromField = fromTextField_ $ Just . fromMaybe (UIThemeEntityOverrides Nothing Nothing) . decodeJSON
fromField = fromTextField_ $ Just . fromMaybe (UIThemeEntityOverrides Nothing Nothing Nothing) . decodeJSON
+17 -8
View File
@@ -1190,16 +1190,17 @@ simplexDomainLine nameType (Just SimplexDomainClaim {domain, proof}) status = ca
-- TODO [short links] show all settings
viewAddressSettings :: AddressSettings -> [StyledString]
viewAddressSettings AddressSettings {businessAddress, autoAccept, autoReply} = case autoAccept of
viewAddressSettings AddressSettings {businessAddress, autoAccept, chatLayout, autoReply} = case autoAccept of
Just AutoAccept {acceptIncognito} ->
("auto_accept on" <> aaInfo)
: maybe [] ((["auto reply:"] <>) . ttyMsgContent) autoReply
: viewChatLayout chatLayout
<> maybe [] ((["auto reply:"] <>) . ttyMsgContent) autoReply
where
aaInfo
| businessAddress = ", business"
| acceptIncognito = ", incognito"
| otherwise = ""
_ -> ["auto_accept off"]
_ -> "auto_accept off" : viewChatLayout chatLayout
groupLink_ :: Bool -> StyledString -> GroupInfo -> GroupLink -> [StyledString]
groupLink_ showFullLinks intro g GroupLink {connLinkContact = CCLink cReq shortLink, acceptMemberRole} =
@@ -1893,7 +1894,7 @@ dayTime :: UTCTime -> Text
dayTime = T.pack . formatTime defaultTimeLocale "%Y-%m-%d %H:%M"
viewContactInfo :: Contact -> Maybe ConnectionStats -> Maybe Profile -> [StyledString]
viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink, localBadge, contactDomain, contactDomainVerified, description}, activeConn, uiThemes, customData} stats incognitoProfile =
viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink, localBadge, contactDomain, contactDomainVerified, description}, activeConn, uiThemes, chatLayout, customData} stats incognitoProfile =
["contact ID: " <> sShow contactId]
<> viewContactBadge localBadge
<> maybe [] ((bold' "description:" :) . map plain . T.lines) description
@@ -1909,6 +1910,7 @@ viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, conta
<> ["quantum resistant end-to-end encryption" | contactPQEnabled ct == CR.PQEncOn]
<> maybe [] (\ac -> [viewPeerChatVRange (peerChatVRange ac)]) activeConn
<> viewUITheme uiThemes
<> viewChatLayout chatLayout
<> viewCustomData customData
viewGroupInfo :: GroupInfo -> [StyledString]
@@ -1931,6 +1933,9 @@ viewGroupInfo gInfo@GroupInfo {groupId, businessChat, groupDomainVerified, group
viewUITheme :: Maybe UIThemeEntityOverrides -> [StyledString]
viewUITheme = maybe [] (\uiThemes -> ["UI themes: " <> viewJSON uiThemes])
viewChatLayout :: Maybe ChatLayout -> [StyledString]
viewChatLayout = maybe [] (\chatLayout -> ["chat layout: " <> plain (textEncode chatLayout)])
viewCustomData :: Maybe CustomData -> [StyledString]
viewCustomData = maybe [] (\(CustomData v) -> ["custom data: " <> viewJSON (J.Object v)])
@@ -2111,10 +2116,10 @@ countactUserPrefText cup = case cup of
viewGroupUpdated :: GroupInfo -> GroupInfo -> Maybe GroupMember -> Maybe MsgSigStatus -> [StyledString]
viewGroupUpdated
GroupInfo {localDisplayName = n, groupProfile = GroupProfile {fullName, shortDescr, description, image, groupPreferences = gps, memberAdmission = ma, publicGroup = pg}}
g'@GroupInfo {localDisplayName = n', groupProfile = GroupProfile {fullName = fullName', shortDescr = shortDescr', description = description', image = image', groupPreferences = gps', memberAdmission = ma', publicGroup = pg'}}
GroupInfo {localDisplayName = n, groupProfile = GroupProfile {fullName, shortDescr, description, image, groupPreferences = gps, memberAdmission = ma, chatLayout = layout, publicGroup = pg}}
g'@GroupInfo {localDisplayName = n', groupProfile = GroupProfile {fullName = fullName', shortDescr = shortDescr', description = description', image = image', groupPreferences = gps', memberAdmission = ma', chatLayout = layout', publicGroup = pg'}}
m signed = do
let update = groupProfileUpdated <> groupPrefsUpdated <> memberAdmissionUpdated <> publicGroupAccessUpdated
let update = groupProfileUpdated <> groupPrefsUpdated <> memberAdmissionUpdated <> chatLayoutUpdated <> publicGroupAccessUpdated
if null update
then []
else memberUpdated <> update
@@ -2139,6 +2144,9 @@ viewGroupUpdated
memberAdmissionUpdated
| ma == ma' = []
| otherwise = ["changed member admission rules"]
chatLayoutUpdated
| layout == layout' = []
| otherwise = ["chat layout " <> maybe "removed" (("changed to: " <>) . plain . textEncode) layout']
publicGroupAccessUpdated
| access == access' = []
| otherwise = ["updated public group access:" <> viewAccess access']
@@ -2153,11 +2161,12 @@ viewGroupUpdated
<> (if allowEmbedding then " embed=on" else "")
viewGroupProfile :: GroupInfo -> [StyledString]
viewGroupProfile g@GroupInfo {groupProfile = GroupProfile {shortDescr, description, image, groupPreferences = gps}} =
viewGroupProfile g@GroupInfo {groupProfile = GroupProfile {shortDescr, description, image, groupPreferences = gps, chatLayout}} =
[ttyFullGroup g]
<> maybe [] (\sd -> ["description: " <> plain sd]) shortDescr
<> maybe [] (const ["has profile image"]) image
<> maybe [] ((bold' "welcome message:" :) . map plain . T.lines) description
<> viewChatLayout chatLayout
<> (bold' "group preferences:" : map viewPref (if useRelays' g then channelGroupFeatures else regularGroupFeatures))
where
viewPref (AGF f) = plain $ groupPreferenceText (pref gps)