From f0799ef2a5ff8636794aa438bfcf2fb5d05907a8 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 16 Mar 2026 19:23:00 +0000 Subject: [PATCH 1/6] website: improve first page load (#6680) * website: improve first page load * remove low res images * simplify * fix buttons jitter * fix color scheme toggling * fix other pages --------- Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com> --- website/src/_includes/dark-mode.html | 1 + website/src/_includes/layouts/article.html | 2 ++ website/src/_includes/layouts/doc.html | 2 ++ website/src/_includes/layouts/jobs.html | 2 ++ website/src/_includes/layouts/main.html | 2 ++ website/src/_includes/layouts/privacy.html | 2 ++ website/src/_includes/layouts/token.html | 2 ++ website/src/_includes/navbar.html | 16 ++++++++++ website/src/css/design3-nav.css | 4 +-- website/src/css/design3.css | 34 +++++++++++++--------- website/src/index.html | 32 ++++++++++++++++++-- 11 files changed, 82 insertions(+), 17 deletions(-) create mode 100644 website/src/_includes/dark-mode.html diff --git a/website/src/_includes/dark-mode.html b/website/src/_includes/dark-mode.html new file mode 100644 index 0000000000..744eaa2b5e --- /dev/null +++ b/website/src/_includes/dark-mode.html @@ -0,0 +1 @@ + diff --git a/website/src/_includes/layouts/article.html b/website/src/_includes/layouts/article.html index 72d9b10b78..a54cad9a77 100644 --- a/website/src/_includes/layouts/article.html +++ b/website/src/_includes/layouts/article.html @@ -8,6 +8,7 @@ + {% include "dark-mode.html" %} SimpleX blog: {{ title }} @@ -23,6 +24,7 @@ {% endif %} + diff --git a/website/src/_includes/layouts/doc.html b/website/src/_includes/layouts/doc.html index 017526d164..e148867f17 100644 --- a/website/src/_includes/layouts/doc.html +++ b/website/src/_includes/layouts/doc.html @@ -9,12 +9,14 @@ + {% include "dark-mode.html" %} {{ title }} + diff --git a/website/src/_includes/layouts/jobs.html b/website/src/_includes/layouts/jobs.html index 74ec29a23e..5aa3ff286b 100644 --- a/website/src/_includes/layouts/jobs.html +++ b/website/src/_includes/layouts/jobs.html @@ -8,12 +8,14 @@ + {% include "dark-mode.html" %} {{ title }} + diff --git a/website/src/_includes/layouts/main.html b/website/src/_includes/layouts/main.html index 26a0d034bc..d51c934446 100644 --- a/website/src/_includes/layouts/main.html +++ b/website/src/_includes/layouts/main.html @@ -7,6 +7,7 @@ {% endfor %}> + {% include "dark-mode.html" %} {{ title }} @@ -25,6 +26,7 @@ + diff --git a/website/src/_includes/layouts/privacy.html b/website/src/_includes/layouts/privacy.html index 7b41c6e5bc..d226459dc1 100644 --- a/website/src/_includes/layouts/privacy.html +++ b/website/src/_includes/layouts/privacy.html @@ -8,6 +8,7 @@ + {% include "dark-mode.html" %} SimpleX Privacy Policy @@ -23,6 +24,7 @@ {% endif %} + diff --git a/website/src/_includes/layouts/token.html b/website/src/_includes/layouts/token.html index 62e8e386b3..fa884271e0 100644 --- a/website/src/_includes/layouts/token.html +++ b/website/src/_includes/layouts/token.html @@ -8,6 +8,7 @@ + {% include "dark-mode.html" %} {{ title }} @@ -19,6 +20,7 @@ + diff --git a/website/src/_includes/navbar.html b/website/src/_includes/navbar.html index 16782d3771..9bfe8a3913 100644 --- a/website/src/_includes/navbar.html +++ b/website/src/_includes/navbar.html @@ -218,6 +218,20 @@ function iconToggle () { moonIcon.classList.toggle('hidden'); } +function updateThemeColor() { + const meta = document.querySelector('meta[name="theme-color"]'); + if (meta) meta.setAttribute('content', getComputedStyle(document.body).backgroundColor); + const navbar = document.querySelector('header#navbar'); + if (navbar && navbar.classList.contains('open')) { + navbar.style.transition = 'none'; + navbar.classList.remove('open'); + setTimeout(() => { + navbar.classList.add('open'); + setTimeout(() => navbar.style.transition = '', 0); + }, 0); + } +} + function themeCheck() { if (userTheme === 'dark' || (!userTheme && systemTheme)) { document.documentElement.classList.add('dark'); @@ -232,6 +246,7 @@ function themeCheck() { prismThemeLink.setAttribute('href','/css/prism-light.min.css') } } + updateThemeColor(); } themeCheck(); @@ -251,6 +266,7 @@ function themeSwitch () { } iconToggle(); } + updateThemeColor(); } const nav = document.querySelector('header#navbar'); diff --git a/website/src/css/design3-nav.css b/website/src/css/design3-nav.css index e6558823fc..6fd3826566 100644 --- a/website/src/css/design3-nav.css +++ b/website/src/css/design3-nav.css @@ -585,7 +585,7 @@ button#cross-btn { } header#navbar nav#menu .nav-link a { - font-family: "Manrope", "GT-Walsheim", sans-serif; + font-family: "Manrope", sans-serif; font-weight: 300; font-size: 18px; width: 100%; @@ -638,7 +638,7 @@ button#cross-btn { } header#navbar nav#menu .sub-menu li a { - font-family: "Manrope", "GT-Walsheim", sans-serif; + font-family: "Manrope", sans-serif; font-weight: 300; width: 100%; } diff --git a/website/src/css/design3.css b/website/src/css/design3.css index d4b5d4d271..d2d9b65aac 100644 --- a/website/src/css/design3.css +++ b/website/src/css/design3.css @@ -216,10 +216,6 @@ html { font-family: GT-Walsheim, Gilroy, Helvetica, sans-serif; } -html, -body { - background: #ffffff; -} .dark html, .dark body { @@ -397,8 +393,8 @@ section.cover div.content { } section.cover div.content h1 { - font-family: "GT-Walsheim", "Manrope", sans-serif; - font-weight: 600; + font-family: "GT-Walsheim", sans-serif; + font-weight: 700; font-size: calc(var(--sec-vwu) * 11.7); line-height: 0.9; letter-spacing: -0.025em; @@ -419,7 +415,7 @@ section.cover div.content h1 .medium { } section.cover div.content h2 { - font-family: "GT-Walsheim", "Manrope", sans-serif; + font-family: "GT-Walsheim", sans-serif; font-weight: 400; font-size: calc(var(--sec-vwu) * 5); letter-spacing: -0.025em; @@ -427,7 +423,7 @@ section.cover div.content h2 { } section.cover div.content p { - font-family: "Manrope", "GT-Walsheim", sans-serif; + font-family: "Manrope", sans-serif; font-weight: 200; font-size: calc(var(--sec-vwu) * 2.14); align-items: center; @@ -491,7 +487,7 @@ section.cover div.content p { .security-audits { font-size: 14px !important; - font-family: 'Manrope', 'GT-Walsheim', sans-serif !important; + font-family: 'Manrope', sans-serif !important; font-weight: 300 !important; color: white; line-height: 1.2; @@ -532,6 +528,14 @@ section.cover div.content p { flex-wrap: wrap; } +@media (min-width: 960px) { + .socials .desktop-app-btn, + .socials .apple-store-btn, + .socials .google-play-btn { + display: flex !important; + } +} + [dir="ltr"] .socials { right: 30px; } @@ -567,7 +571,7 @@ section.cover div.content p { .desktop-app-btn .btn-content p { margin: 0; font-size: 11px !important; - font-family: 'Manrope', 'GT-Walsheim', sans-serif !important; + font-family: 'Manrope', sans-serif !important; font-weight: 300 !important; line-height: 1.2 !important; text-align: left; @@ -593,6 +597,10 @@ section.cover div.content p { .socials { position: static; } + + .socials { + min-height: 42px; + } } /* --- MAIN SECTIONS --- */ @@ -761,7 +769,7 @@ main .section-bg { } .page .text-container h2 { - font-family: "GT-Walsheim", "Manrope", sans-serif; + font-family: "GT-Walsheim", sans-serif; font-weight: 300; font-size: calc(var(--sec-vwu)*4.94); letter-spacing: -0.025em; @@ -775,7 +783,7 @@ main .section-bg { } .page .text-container p { - font-family: "Manrope", "GT-Walsheim", sans-serif; + font-family: "Manrope", sans-serif; font-weight: 200; font-size: calc(var(--sec-vwu)*1.62); max-width: calc(var(--sec-vwu)*23); @@ -791,7 +799,7 @@ main .section-bg { } .page .text-container a { - font-family: "Manrope", "GT-Walsheim", sans-serif; + font-family: "Manrope", sans-serif; font-weight: 200; font-size: calc(var(--sec-vwu)*1.62); text-decoration: underline; diff --git a/website/src/index.html b/website/src/index.html index 347874a6f7..7e0e5d524b 100644 --- a/website/src/index.html +++ b/website/src/index.html @@ -6,7 +6,7 @@ active_home: true --- - + {% include "dark-mode.html" %} - {{ title }} @@ -34,6 +34,34 @@ active_home: true + + + + + + + + + + + + + + + + + + + + + + + + + + + + From b8178d01a885165c20c69fe9a95963565cce62ad Mon Sep 17 00:00:00 2001 From: Ed Asriyan Date: Thu, 19 Mar 2026 02:08:14 -0700 Subject: [PATCH 2/6] core: fix `/_groups` command (#6660) * core: fix `/_groups` command add missing space after `/_groups`. fixes #5195 * remove space in CLI commands (they would break parser) --------- Co-authored-by: Evgeny --- src/Simplex/Chat/Library/Commands.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index e5e8d60ad7..e7e68554f0 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -4526,7 +4526,7 @@ chatCommandP = "/clear " *> char_ '@' *> (ClearContact <$> displayNameP), ("/members " <|> "/ms ") *> char_ '#' *> (ListMembers <$> displayNameP), "/member support chats #" *> (ListMemberSupportChats <$> displayNameP), - "/_groups" *> (APIListGroups <$> A.decimal <*> optional (" @" *> A.decimal) <*> optional (A.space *> textP)), + "/_groups " *> (APIListGroups <$> A.decimal <*> optional (" @" *> A.decimal) <*> optional (A.space *> textP)), ("/groups" <|> "/gs") *> (ListGroups <$> optional (" @" *> displayNameP) <*> optional (A.space *> textP)), "/_group_profile #" *> (APIUpdateGroupProfile <$> A.decimal <* A.space <*> jsonP), ("/group_profile " <|> "/gp ") *> char_ '#' *> (UpdateGroupNames <$> displayNameP <* A.space <*> groupProfile), From 2df13dad36f306f5f73a549f9fd4d376f4f6df32 Mon Sep 17 00:00:00 2001 From: sh <37271604+shumvgolove@users.noreply.github.com> Date: Thu, 19 Mar 2026 09:10:19 +0000 Subject: [PATCH 3/6] core: add custom data commands, fix groups parser (#6691) * core: add custom data commands, fix groups parser - Add APISetGroupCustomData and APISetContactCustomData to ChatCommand, with parsers (/_set custom #, /_set custom @) and processors following the APISetChatUIThemes pattern - Fix APIListGroups parser missing space ("/_groups" -> "/_groups ") to align with auto-generated cmdString - Add chatCommandsDocsData entries for APISetGroupCustomData, APISetContactCustomData, and APISetUserAutoAcceptMemberContacts * core: named fields for codegen, run codegen - Use named record fields for APISetGroupCustomData, APISetContactCustomData, APISetUserAutoAcceptMemberContacts (required for chatCommandsDocsData field resolution) - Fix OnOff field name to "onOff" (avoids clash with User field) - Remove APISetUserAutoAcceptMemberContacts from undocumentedCommands - Regenerate COMMANDS.md and commands.ts * nodejs: add ChatApi wrappers for custom data and apiGetChat - apiSetGroupCustomData, apiSetContactCustomData - apiSetAutoAcceptMemberContacts - apiGetChat (manual wrapper, APIGetChat undocumented) --- bots/api/COMMANDS.md | 114 ++++++++++++++++++ bots/src/API/Docs/Commands.hs | 6 +- .../types/typescript/src/commands.ts | 45 +++++++ packages/simplex-chat-nodejs/src/api.ts | 41 +++++++ src/Simplex/Chat/Controller.hs | 4 +- src/Simplex/Chat/Library/Commands.hs | 12 ++ 6 files changed, 219 insertions(+), 3 deletions(-) diff --git a/bots/api/COMMANDS.md b/bots/api/COMMANDS.md index b408d8eb30..9f63770df6 100644 --- a/bots/api/COMMANDS.md +++ b/bots/api/COMMANDS.md @@ -50,6 +50,9 @@ This file is generated automatically. - [APIListContacts](#apilistcontacts) - [APIListGroups](#apilistgroups) - [APIDeleteChat](#apideletechat) +- [APISetGroupCustomData](#apisetgroupcustomdata) +- [APISetContactCustomData](#apisetcontactcustomdata) +- [APISetUserAutoAcceptMemberContacts](#apisetuserautoacceptmembercontacts) [User profile commands](#user-profile-commands) - [ShowActiveUser](#showactiveuser) @@ -1526,6 +1529,117 @@ ChatCmdError: Command error (only used in WebSockets API). --- +### APISetGroupCustomData + +Set group custom data. + +*Network usage*: no. + +**Parameters**: +- groupId: int64 +- customData: JSONObject? + +**Syntax**: + +``` +/_set custom #[ ] +``` + +```javascript +'/_set custom #' + groupId + (customData ? ' ' + JSON.stringify(customData) : '') // JavaScript +``` + +```python +'/_set custom #' + str(groupId) + ((' ' + json.dumps(customData)) if customData is not None else '') # Python +``` + +**Responses**: + +CmdOk: Ok. +- type: "cmdOk" +- user_: [User](./TYPES.md#user)? + +ChatCmdError: Command error (only used in WebSockets API). +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + +--- + + +### APISetContactCustomData + +Set contact custom data. + +*Network usage*: no. + +**Parameters**: +- contactId: int64 +- customData: JSONObject? + +**Syntax**: + +``` +/_set custom @[ ] +``` + +```javascript +'/_set custom @' + contactId + (customData ? ' ' + JSON.stringify(customData) : '') // JavaScript +``` + +```python +'/_set custom @' + str(contactId) + ((' ' + json.dumps(customData)) if customData is not None else '') # Python +``` + +**Responses**: + +CmdOk: Ok. +- type: "cmdOk" +- user_: [User](./TYPES.md#user)? + +ChatCmdError: Command error (only used in WebSockets API). +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + +--- + + +### APISetUserAutoAcceptMemberContacts + +Set auto-accept member contacts. + +*Network usage*: no. + +**Parameters**: +- userId: int64 +- onOff: bool + +**Syntax**: + +``` +/_set accept member contacts on|off +``` + +```javascript +'/_set accept member contacts ' + userId + ' ' + (onOff ? 'on' : 'off') // JavaScript +``` + +```python +'/_set accept member contacts ' + str(userId) + ' ' + ('on' if onOff else 'off') # Python +``` + +**Responses**: + +CmdOk: Ok. +- type: "cmdOk" +- user_: [User](./TYPES.md#user)? + +ChatCmdError: Command error (only used in WebSockets API). +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + +--- + + ## User profile commands Most bots don't need to use these commands, as bot profile can be configured manually via CLI or desktop client. These commands can be used by bots that need to manage multiple user profiles (e.g., the profiles of support agents). diff --git a/bots/src/API/Docs/Commands.hs b/bots/src/API/Docs/Commands.hs index 35745f9b42..77b5f66417 100644 --- a/bots/src/API/Docs/Commands.hs +++ b/bots/src/API/Docs/Commands.hs @@ -142,7 +142,10 @@ chatCommandsDocsData = "Commands to list and delete conversations.", [ ("APIListContacts", [], "Get contacts.", ["CRContactsList", "CRChatCmdError"], [], Nothing, "/_contacts " <> Param "userId"), ("APIListGroups", [], "Get groups.", ["CRGroupsList", "CRChatCmdError"], [], Nothing, "/_groups " <> Param "userId" <> Optional "" (" @" <> Param "$0") "contactId_" <> Optional "" (" " <> Param "$0") "search"), - ("APIDeleteChat", [], "Delete chat.", ["CRContactDeleted", "CRContactConnectionDeleted", "CRGroupDeletedUser", "CRChatCmdError"], [], Just UNBackground, "/_delete " <> Param "chatRef" <> " " <> Param "chatDeleteMode") + ("APIDeleteChat", [], "Delete chat.", ["CRContactDeleted", "CRContactConnectionDeleted", "CRGroupDeletedUser", "CRChatCmdError"], [], Just UNBackground, "/_delete " <> Param "chatRef" <> " " <> Param "chatDeleteMode"), + ("APISetGroupCustomData", [], "Set group custom data.", ["CRCmdOk", "CRChatCmdError"], [], Nothing, "/_set custom #" <> Param "groupId" <> Optional "" (" " <> Json "$0") "customData"), + ("APISetContactCustomData", [], "Set contact custom data.", ["CRCmdOk", "CRChatCmdError"], [], Nothing, "/_set custom @" <> Param "contactId" <> Optional "" (" " <> Json "$0") "customData"), + ("APISetUserAutoAcceptMemberContacts", [], "Set auto-accept member contacts.", ["CRCmdOk", "CRChatCmdError"], [], Nothing, "/_set accept member contacts " <> Param "userId" <> " " <> OnOff "onOff") -- ("APIChatItemsRead", [], "Mark items as read.", ["CRItemsReadForChat"], [], Nothing, ""), -- ("APIChatRead", [], "Mark chat as read.", ["CRCmdOk"], [], Nothing, ""), -- ("APIChatUnread", [], "Mark chat as unread.", ["CRCmdOk"], [], Nothing, ""), @@ -398,7 +401,6 @@ undocumentedCommands = "APISetServerOperators", "APISetUserContactReceipts", "APISetUserGroupReceipts", - "APISetUserAutoAcceptMemberContacts", "APISetUserServers", "APISetUserUIThemes", "APIStandaloneFileInfo", diff --git a/packages/simplex-chat-client/types/typescript/src/commands.ts b/packages/simplex-chat-client/types/typescript/src/commands.ts index 66f4f6ec5f..edeabe7837 100644 --- a/packages/simplex-chat-client/types/typescript/src/commands.ts +++ b/packages/simplex-chat-client/types/typescript/src/commands.ts @@ -557,6 +557,51 @@ export namespace APIDeleteChat { } } +// Set group custom data. +// Network usage: no. +export interface APISetGroupCustomData { + groupId: number // int64 + customData?: object +} + +export namespace APISetGroupCustomData { + export type Response = CR.CmdOk | CR.ChatCmdError + + export function cmdString(self: APISetGroupCustomData): string { + return '/_set custom #' + self.groupId + (self.customData ? ' ' + JSON.stringify(self.customData) : '') + } +} + +// Set contact custom data. +// Network usage: no. +export interface APISetContactCustomData { + contactId: number // int64 + customData?: object +} + +export namespace APISetContactCustomData { + export type Response = CR.CmdOk | CR.ChatCmdError + + export function cmdString(self: APISetContactCustomData): string { + return '/_set custom @' + self.contactId + (self.customData ? ' ' + JSON.stringify(self.customData) : '') + } +} + +// Set auto-accept member contacts. +// Network usage: no. +export interface APISetUserAutoAcceptMemberContacts { + userId: number // int64 + onOff: boolean +} + +export namespace APISetUserAutoAcceptMemberContacts { + export type Response = CR.CmdOk | CR.ChatCmdError + + export function cmdString(self: APISetUserAutoAcceptMemberContacts): string { + return '/_set accept member contacts ' + self.userId + ' ' + (self.onOff ? 'on' : 'off') + } +} + // User profile commands // Most bots don't need to use these commands, as bot profile can be configured manually via CLI or desktop client. These commands can be used by bots that need to manage multiple user profiles (e.g., the profiles of support agents). diff --git a/packages/simplex-chat-nodejs/src/api.ts b/packages/simplex-chat-nodejs/src/api.ts index dc87055f87..c3e85b3915 100644 --- a/packages/simplex-chat-nodejs/src/api.ts +++ b/packages/simplex-chat-nodejs/src/api.ts @@ -747,6 +747,47 @@ export class ChatApi { throw new ChatCommandError("error deleting chat", r) } + /** + * Set group custom data. + * Network usage: no. + */ + async apiSetGroupCustomData(groupId: number, customData?: object): Promise { + const r = await this.sendChatCmd(CC.APISetGroupCustomData.cmdString({groupId, customData})) + if (r.type === "cmdOk") return + throw new ChatCommandError("error setting group custom data", r) + } + + /** + * Set contact custom data. + * Network usage: no. + */ + async apiSetContactCustomData(contactId: number, customData?: object): Promise { + const r = await this.sendChatCmd(CC.APISetContactCustomData.cmdString({contactId, customData})) + if (r.type === "cmdOk") return + throw new ChatCommandError("error setting contact custom data", r) + } + + /** + * Set auto-accept member contacts. + * Network usage: no. + */ + async apiSetAutoAcceptMemberContacts(userId: number, onOff: boolean): Promise { + const r = await this.sendChatCmd(CC.APISetUserAutoAcceptMemberContacts.cmdString({userId, onOff})) + if (r.type === "cmdOk") return + throw new ChatCommandError("error setting auto-accept member contacts", r) + } + + /** + * Get chat items. + * Network usage: no. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async apiGetChat(chatType: T.ChatType, chatId: number, count: number): Promise { + const r: any = await this.sendChatCmd(`/_get chat ${T.ChatType.cmdString(chatType)}${chatId} count=${count}`) + if (r.type === "apiChat") return r.chat + throw new ChatCommandError("error getting chat", r) + } + /** * Get active user profile * Network usage: no. diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 9703226e1a..24dfaf46a1 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -275,7 +275,7 @@ data ChatCommand | SetUserContactReceipts UserMsgReceiptSettings | APISetUserGroupReceipts UserId UserMsgReceiptSettings | SetUserGroupReceipts UserMsgReceiptSettings - | APISetUserAutoAcceptMemberContacts UserId Bool + | APISetUserAutoAcceptMemberContacts {userId :: UserId, onOff :: Bool} | SetUserAutoAcceptMemberContacts Bool | APIHideUser UserId UserPwd | APIUnhideUser UserId UserPwd @@ -362,6 +362,8 @@ data ChatCommand | APISetConnectionAlias {connectionId :: Int64, localAlias :: LocalAlias} | APISetUserUIThemes UserId (Maybe UIThemeEntityOverrides) | APISetChatUIThemes ChatRef (Maybe UIThemeEntityOverrides) + | APISetGroupCustomData {groupId :: GroupId, customData :: Maybe CustomData} + | APISetContactCustomData {contactId :: ContactId, customData :: Maybe CustomData} | APIGetNtfToken | APIRegisterToken DeviceToken NotificationsMode | APIVerifyToken DeviceToken C.CbNonce ByteString diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index e7e68554f0..fce4f8438c 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -1409,6 +1409,16 @@ processChatCommand vr nm = \case liftIO $ setGroupUIThemes db user g uiThemes ok user _ -> throwCmdError "not supported" + APISetGroupCustomData groupId customData_ -> withUser $ \user -> do + withFastStore $ \db -> do + g <- getGroupInfo db vr user groupId + liftIO $ setGroupCustomData db user g customData_ + ok user + APISetContactCustomData contactId customData_ -> withUser $ \user -> do + withFastStore $ \db -> do + ct <- getContact db vr user contactId + liftIO $ setContactCustomData db user ct customData_ + ok user APIGetNtfToken -> withUser' $ \_ -> crNtfToken <$> withAgent getNtfToken APIRegisterToken token mode -> withUser $ \_ -> CRNtfTokenStatus <$> withAgent (\a -> registerNtfToken a nm token mode) @@ -4415,6 +4425,8 @@ chatCommandP = "/_set prefs @" *> (APISetContactPrefs <$> A.decimal <* A.space <*> jsonP), "/_set theme user " *> (APISetUserUIThemes <$> A.decimal <*> optional (A.space *> jsonP)), "/_set theme " *> (APISetChatUIThemes <$> chatRefP <*> optional (A.space *> jsonP)), + "/_set custom #" *> (APISetGroupCustomData <$> A.decimal <*> optional (A.space *> jsonP)), + "/_set custom @" *> (APISetContactCustomData <$> A.decimal <*> optional (A.space *> jsonP)), "/_ntf get" $> APIGetNtfToken, "/_ntf register " *> (APIRegisterToken <$> strP_ <*> strP), "/_ntf verify " *> (APIVerifyToken <$> strP <* A.space <*> strP <* A.space <*> strP), From 06bbe764b92703ca0e3c6dc333cb851e7c9fcd10 Mon Sep 17 00:00:00 2001 From: sh <37271604+shumvgolove@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:11:39 +0000 Subject: [PATCH 4/6] website: update xftp-web and enable file in navbar (#6694) * website: update xftp-web and enable file in navbar * update link etc * website: translate send-file key --------- Co-authored-by: Evgeny Poberezkin --- docs/DOWNLOADS.md | 1 - website/langs/ar.json | 3 ++- website/langs/cs.json | 3 ++- website/langs/de.json | 3 ++- website/langs/en.json | 6 +++--- website/langs/es.json | 3 ++- website/langs/fi.json | 3 ++- website/langs/fr.json | 3 ++- website/langs/he.json | 3 ++- website/langs/hu.json | 3 ++- website/langs/id.json | 3 ++- website/langs/it.json | 3 ++- website/langs/ja.json | 3 ++- website/langs/nl.json | 3 ++- website/langs/pl.json | 3 ++- website/langs/pt_BR.json | 3 ++- website/langs/ru.json | 3 ++- website/langs/uk.json | 3 ++- website/langs/zh_Hans.json | 3 ++- website/package.json | 2 +- website/src/_includes/navbar.html | 8 ++++---- website/src/css/design3-nav.css | 8 ++++++++ 22 files changed, 50 insertions(+), 26 deletions(-) diff --git a/docs/DOWNLOADS.md b/docs/DOWNLOADS.md index b9889fe7a7..f0e9466c61 100644 --- a/docs/DOWNLOADS.md +++ b/docs/DOWNLOADS.md @@ -4,7 +4,6 @@ permalink: /downloads/index.html revision: 09.09.2024 --- -| Updated 09.09.2024 | Languages: EN | # Download SimpleX apps You can get the latest beta releases from [GitHub](https://github.com/simplex-chat/simplex-chat/releases). diff --git a/website/langs/ar.json b/website/langs/ar.json index c6549a8df5..708e2d306d 100644 --- a/website/langs/ar.json +++ b/website/langs/ar.json @@ -313,5 +313,6 @@ "index-roadmap-2027": "2027", "navbar-token": "رمز", "index-token-cta": "تعرف على المزيد واحصل على NFT مجاني
للاختبار المبكر.", - "navbar-old-site": "الموقع القديم" + "navbar-old-site": "الموقع القديم", + "send-file": "إرسال ملف" } diff --git a/website/langs/cs.json b/website/langs/cs.json index 7e141b581c..fd3cde171e 100644 --- a/website/langs/cs.json +++ b/website/langs/cs.json @@ -313,5 +313,6 @@ "messengers-comparison-section-list-point-4": "Podpora více zařízení může oslabit post-kompromitační bezpečnost protokolu Double Ratchet", "messengers-comparison-section-list-point-5": "Dvoufaktorovou výměnu klíčů lze volitelně aktivovat pomocí ověření bezpečnostním kódem.", "messengers-comparison-section-list-point-6": "Postkvantová dohoda o klíči je omezená — chrání pouze některé kroky ratchet mechanismu.", - "navbar-old-site": "Starý web" + "navbar-old-site": "Starý web", + "send-file": "Odeslat soubor" } diff --git a/website/langs/de.json b/website/langs/de.json index 42ff308014..a1ec3447b3 100644 --- a/website/langs/de.json +++ b/website/langs/de.json @@ -313,5 +313,6 @@ "messengers-comparison-section-list-point-5": "Ein 2-Faktor-Schlüsselaustausch ist optional und kann über die Überprüfung eines Sicherheitscodes erfolgen.", "messengers-comparison-section-list-point-6": "Die Schlüsselvereinbarung per Post-Quanten-Security ist nicht durchgängig — Sie schützt lediglich ausgewählte Schritte innerhalb des Ratchet-Prozesses.", "navbar-token": "Token", - "navbar-old-site": "Alte Webseite" + "navbar-old-site": "Alte Webseite", + "send-file": "Datei senden" } diff --git a/website/langs/en.json b/website/langs/en.json index da4a04fc62..b509e90342 100644 --- a/website/langs/en.json +++ b/website/langs/en.json @@ -325,7 +325,7 @@ "why-p8": "Because we destroyed the power to know who you are. So that your power can never be taken.", "why-tagline": "Be free in your network.", "why-footer-link": "Why we are building it", - "file": "File", + "send-file": "Send file", "file-desc": "Send files securely with end-to-end encryption — no accounts, no tracking.", "file-noscript": "JavaScript is required for file transfer.", "file-e2e-note": "End-to-end encrypted — the server never sees your file.", @@ -336,7 +336,7 @@ "file-drop-text": "Drag & drop a file here", "file-drop-hint": "or", "file-choose": "Choose file", - "file-max-size": "Max 100 MB - SimpleX Chat app supports files up to 1 GB", + "file-max-size": "Max 100 MB - SimpleX Chat app supports files up to 1 GB", "file-encrypting": "Encrypting\u2026", "file-uploading": "Uploading\u2026", "file-cancel": "Cancel", @@ -347,7 +347,7 @@ "file-expiry": "Files are typically available for 48 hours.", "file-sec-1": "Your file was encrypted in the browser - data routers never see file contents, name or size.", "file-sec-2": "The encryption key is in the link\u2019s hash fragment - it is never sent to any server.", - "file-sec-3": "For better security, use SimpleX Chat app.", + "file-sec-3": "For better security, use SimpleX Chat app.", "file-retry": "Retry", "file-downloading": "Downloading\u2026", "file-decrypting": "Decrypting\u2026", diff --git a/website/langs/es.json b/website/langs/es.json index f36ee7c962..ac6993c085 100644 --- a/website/langs/es.json +++ b/website/langs/es.json @@ -313,5 +313,6 @@ "messengers-comparison-section-list-point-5": "El intercambio de clave de doble factor es opcional mediante la verificación del código de seguridad.", "messengers-comparison-section-list-point-6": "El acuerdo de claves postcuántico es \"parcial\", solo protege determinados pasos del ratchet.", "navbar-token": "Token", - "navbar-old-site": "Web antigua" + "navbar-old-site": "Web antigua", + "send-file": "Enviar archivo" } diff --git a/website/langs/fi.json b/website/langs/fi.json index 10697898cb..e1e31c633b 100644 --- a/website/langs/fi.json +++ b/website/langs/fi.json @@ -257,5 +257,6 @@ "docs-dropdown-10": "Läpinäkyvyys", "docs-dropdown-11": "Usein kysytyt kysymykset", "docs-dropdown-12": "Turvallisuus", - "docs-dropdown-14": "SimpleX yrityksille" + "docs-dropdown-14": "SimpleX yrityksille", + "send-file": "Lähetä tiedosto" } diff --git a/website/langs/fr.json b/website/langs/fr.json index fc1c6301f7..8f87809224 100644 --- a/website/langs/fr.json +++ b/website/langs/fr.json @@ -280,5 +280,6 @@ "index-token-h2": "Des communautés qui durent", "index-token-cta": "En savoir plus et obtenez votre NFT gratuit
pour les premiers tests.", "index-roadmap-h2": "Feuille de route de SimpleX vers un Internet libre", - "index-roadmap-2025": "2025" + "index-roadmap-2025": "2025", + "send-file": "Envoyer un fichier" } diff --git a/website/langs/he.json b/website/langs/he.json index 03dc671919..0af1afaec3 100644 --- a/website/langs/he.json +++ b/website/langs/he.json @@ -255,5 +255,6 @@ "docs-dropdown-11": "שאלות ותשובות", "docs-dropdown-12": "אבטחה", "hero-overlay-card-3-p-3": "Trail of bits סקר את הקוד הקריפטוגרפי של פרוטוקולי רשת SimpleX ביולי 2024. קרא עוד.", - "docs-dropdown-14": "SimpleX לעסקים" + "docs-dropdown-14": "SimpleX לעסקים", + "send-file": "שליחת קובץ" } diff --git a/website/langs/hu.json b/website/langs/hu.json index 811662d8a5..c5150cabf9 100644 --- a/website/langs/hu.json +++ b/website/langs/hu.json @@ -313,5 +313,6 @@ "messengers-comparison-section-list-point-5": "A kétlépcsős kulcscsere nem követelmény a biztonsági kód ellenőrzéséhez.", "messengers-comparison-section-list-point-6": "A kvantumbiztos kulcscsere „ritka” — csak a racsnis lépések egy részét védi.", "navbar-token": "Token", - "navbar-old-site": "Régi oldal" + "navbar-old-site": "Régi oldal", + "send-file": "Fájl küldése" } diff --git a/website/langs/id.json b/website/langs/id.json index bbdd195115..614f5937c8 100644 --- a/website/langs/id.json +++ b/website/langs/id.json @@ -313,5 +313,6 @@ "messengers-comparison-section-list-point-5": "Pertukaran kunci 2 faktor bersifat opsional via verifikasi kode keamanan.", "messengers-comparison-section-list-point-6": "Kesepakatan kunci Post-quantum \"jarang\" — hanya melindungi beberapa langkah ratchet.", "navbar-token": "Token", - "navbar-old-site": "Situs lama" + "navbar-old-site": "Situs lama", + "send-file": "Kirim file" } diff --git a/website/langs/it.json b/website/langs/it.json index e8311bbd56..5b292a30a0 100644 --- a/website/langs/it.json +++ b/website/langs/it.json @@ -313,5 +313,6 @@ "messengers-comparison-section-list-point-5": "Lo scambio di chiavi a 2 fattori è facoltativo tramite la verifica del codice di sicurezza.", "messengers-comparison-section-list-point-6": "L'accordo sulle chiavi post-quantistico è “scarno” — protegge solo alcuni dei passaggi del ratchet.", "navbar-token": "Token", - "navbar-old-site": "Sito vecchio" + "navbar-old-site": "Sito vecchio", + "send-file": "Invia file" } diff --git a/website/langs/ja.json b/website/langs/ja.json index 0f7e9ff525..d3ce009eff 100644 --- a/website/langs/ja.json +++ b/website/langs/ja.json @@ -295,5 +295,6 @@ "index-publications-whonix-title": "Whonix推奨", "index-publications-heise-title": "Heise Online の記事", "index-publications-kuketz-title": "Mike Kuketzによるレビュー", - "index-publications-optout-title": "OptOut ポッドキャストインタビュー" + "index-publications-optout-title": "OptOut ポッドキャストインタビュー", + "send-file": "ファイルを送信" } diff --git a/website/langs/nl.json b/website/langs/nl.json index f8297e2841..c0acba7cbd 100644 --- a/website/langs/nl.json +++ b/website/langs/nl.json @@ -257,5 +257,6 @@ "hero-overlay-card-3-p-3": "Trail of Bits heeft in juli 2024 het cryptografische ontwerp van SimpleX-netwerkprotocollen beoordeeld. Lees meer.", "docs-dropdown-14": "SimpleX voor bedrijven", "directory": "Directory", - "about-and-contact-us": "Over ons & Contact" + "about-and-contact-us": "Over ons & Contact", + "send-file": "Bestand verzenden" } diff --git a/website/langs/pl.json b/website/langs/pl.json index 2d82529d26..e14274a408 100644 --- a/website/langs/pl.json +++ b/website/langs/pl.json @@ -313,5 +313,6 @@ "messengers-comparison-section-list-point-4": "Wdrożenie wielu urządzeń zagraża zabezpieczeniu po naruszeniu bezpieczeństwa systemu Double Ratchet", "messengers-comparison-section-list-point-5": "Wymiana kluczy 2-składnikowych jest opcjonalna poprzez weryfikację kodu bezpieczeństwa.", "messengers-comparison-section-list-point-6": "Post-kwantowe, kluczowe porozumienie jest \"rzadkie\" — chroni tylko niektóre kroki systemu Ratchet.", - "navbar-old-site": "Stara strona" + "navbar-old-site": "Stara strona", + "send-file": "Wyślij plik" } diff --git a/website/langs/pt_BR.json b/website/langs/pt_BR.json index 018389db8d..3c1c971f7f 100644 --- a/website/langs/pt_BR.json +++ b/website/langs/pt_BR.json @@ -313,5 +313,6 @@ "messengers-comparison-section-list-point-5": "A troca de chaves de dois fatores é opcional por meio da verificação do código de segurança.", "messengers-comparison-section-list-point-6": "O acordo de chaves pós-quântico é 'esparso' — ele protege apenas algumas das etapas da catraca (ratchet).", "navbar-old-site": "Site antigo", - "index-directory-users-group-title": "Grupos de usuários do SimpleX" + "index-directory-users-group-title": "Grupos de usuários do SimpleX", + "send-file": "Enviar arquivo" } diff --git a/website/langs/ru.json b/website/langs/ru.json index 9c73fb9c82..235492291f 100644 --- a/website/langs/ru.json +++ b/website/langs/ru.json @@ -323,5 +323,6 @@ "why-p7": "Древнейшая человеческая свобода — говорить с другим человеком без слежки — построенная на инфраструктуре, которая не может её предать.", "why-p8": "Потому что мы разрушили саму возможность узнать, кто вы. Чтобы вашу свободу невозможно было отнять.", "why-tagline": "Будь свободен в своей сети.", - "why-footer-link": "Почему мы это строим" + "why-footer-link": "Почему мы это строим", + "send-file": "Отправить файл" } diff --git a/website/langs/uk.json b/website/langs/uk.json index f5347bda87..d36ff6c820 100644 --- a/website/langs/uk.json +++ b/website/langs/uk.json @@ -258,5 +258,6 @@ "docs-dropdown-14": "SimpleX для бізнесу", "directory": "Каталог", "navbar-token": "Токен", - "about-and-contact-us": "Про нас & Контакти" + "about-and-contact-us": "Про нас & Контакти", + "send-file": "Надіслати файл" } diff --git a/website/langs/zh_Hans.json b/website/langs/zh_Hans.json index 6ac8bdf833..5b1eea5969 100644 --- a/website/langs/zh_Hans.json +++ b/website/langs/zh_Hans.json @@ -257,5 +257,6 @@ "hero-overlay-card-3-p-3": "Trail of Bits 于 2024 年 7 月审核了 SimpleX 网络协议的加密设计。了解更多信息。", "docs-dropdown-14": "企业版 SimpleX", "directory": "目录", - "about-and-contact-us": "关于 & 联系我们" + "about-and-contact-us": "关于 & 联系我们", + "send-file": "发送文件" } diff --git a/website/package.json b/website/package.json index aabf1c09fa..9f4a5b12e7 100644 --- a/website/package.json +++ b/website/package.json @@ -29,7 +29,7 @@ "tailwindcss": "3.3.1" }, "dependencies": { - "@simplex-chat/xftp-web": "^0.2.0", + "@simplex-chat/xftp-web": "^0.3.0", "eleventy-plugin-i18n": "^0.1.3", "fs": "^0.0.1-security", "gray-matter": "^4.0.3", diff --git a/website/src/_includes/navbar.html b/website/src/_includes/navbar.html index 9bfe8a3913..79032db8ed 100644 --- a/website/src/_includes/navbar.html +++ b/website/src/_includes/navbar.html @@ -26,13 +26,13 @@ - {#
+
- #} +
diff --git a/website/src/css/design3-nav.css b/website/src/css/design3-nav.css index 6fd3826566..5ac51e3e92 100644 --- a/website/src/css/design3-nav.css +++ b/website/src/css/design3-nav.css @@ -497,6 +497,14 @@ button#cross-btn { height: auto; } +@media (min-width: 960px) { + @media (max-width: 1100px) { + header#navbar nav#menu .nav-link.send-file { + display: none; + } + } +} + @media (max-width: 959px) { #mobile-header { position: fixed; From 2472bee6b6aaa0e4a4be488eb73bdc3925fade06 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 20 Mar 2026 20:13:00 +0000 Subject: [PATCH 5/6] website: layout --- website/src/css/design3-nav.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/css/design3-nav.css b/website/src/css/design3-nav.css index 5ac51e3e92..f6f91a8567 100644 --- a/website/src/css/design3-nav.css +++ b/website/src/css/design3-nav.css @@ -498,7 +498,7 @@ button#cross-btn { } @media (min-width: 960px) { - @media (max-width: 1100px) { + @media (max-width: 1050px) { header#navbar nav#menu .nav-link.send-file { display: none; } From a8a88830279cfd957b57c1c6926bd28c6bdbc0b7 Mon Sep 17 00:00:00 2001 From: "Evgeny @ SimpleX Chat" <259188159+evgeny-simplex@users.noreply.github.com> Date: Sat, 21 Mar 2026 18:06:23 +0000 Subject: [PATCH 6/6] core, ui, website: small text markdown (#6697) * core: small text markdown * ios: small markdown * desktop, android: small markdown * fix font size * small markdown on website * update ios core library * update bot api docs --------- Co-authored-by: Evgeny Poberezkin --- .../Views/Chat/ChatItem/MsgContentView.swift | 5 ++ apps/ios/SimpleX.xcodeproj/project.pbxproj | 16 ++--- apps/ios/SimpleXChat/ChatTypes.swift | 1 + .../chat/simplex/common/model/ChatModel.kt | 2 + .../common/views/chat/item/TextItemView.kt | 1 + bots/api/TYPES.md | 3 + .../types/typescript/src/types.ts | 6 ++ plans/2026-03-21-text-size-markdown.md | 66 +++++++++++++++++++ src/Simplex/Chat/Markdown.hs | 14 ++-- src/Simplex/Chat/Styled.hs | 1 + tests/MarkdownTests.hs | 30 +++++++++ website/src/directory.html | 9 +++ website/src/js/directory.js | 3 + 13 files changed, 143 insertions(+), 14 deletions(-) create mode 100644 plans/2026-03-21-text-size-markdown.md diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift index 852c8bbbac..77bd41c5b8 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift @@ -322,6 +322,7 @@ func messageText( var bold: UIFont? var italic: UIFont? var snippet: UIFont? + var small: UIFont? var mention: UIFont? var secretIdx: Int = 0 for ft in fts { @@ -353,6 +354,10 @@ func messageText( attrs[.backgroundColor] = secretColor } hasSecrets = true + case .small: + small = small ?? UIFont.preferredFont(forTextStyle: .footnote) + attrs[.font] = small + attrs[.foregroundColor] = UIColor.secondaryLabel case let .colored(color): if let c = color.uiColor { attrs[.foregroundColor] = UIColor(c) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 314f1c072c..9265138c53 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -178,8 +178,8 @@ 64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; }; 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; }; 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; }; - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-3esYZFBUokREq84LvcOgzJ-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-3esYZFBUokREq84LvcOgzJ-ghc9.6.3.a */; }; - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-3esYZFBUokREq84LvcOgzJ.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-3esYZFBUokREq84LvcOgzJ.a */; }; + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-IckAKQLBKZZ3c4EBa1qhzo-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-IckAKQLBKZZ3c4EBa1qhzo-ghc9.6.3.a */; }; + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-IckAKQLBKZZ3c4EBa1qhzo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-IckAKQLBKZZ3c4EBa1qhzo.a */; }; 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; }; 64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; }; 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; }; @@ -545,8 +545,8 @@ 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = ""; }; 64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-3esYZFBUokREq84LvcOgzJ-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.9-3esYZFBUokREq84LvcOgzJ-ghc9.6.3.a"; sourceTree = ""; }; - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-3esYZFBUokREq84LvcOgzJ.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.9-3esYZFBUokREq84LvcOgzJ.a"; sourceTree = ""; }; + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-IckAKQLBKZZ3c4EBa1qhzo-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.9-IckAKQLBKZZ3c4EBa1qhzo-ghc9.6.3.a"; sourceTree = ""; }; + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-IckAKQLBKZZ3c4EBa1qhzo.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.9-IckAKQLBKZZ3c4EBa1qhzo.a"; sourceTree = ""; }; 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = ""; }; 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = ""; }; @@ -708,8 +708,8 @@ 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */, 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */, 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */, - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-3esYZFBUokREq84LvcOgzJ-ghc9.6.3.a in Frameworks */, - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-3esYZFBUokREq84LvcOgzJ.a in Frameworks */, + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-IckAKQLBKZZ3c4EBa1qhzo-ghc9.6.3.a in Frameworks */, + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-IckAKQLBKZZ3c4EBa1qhzo.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -795,8 +795,8 @@ 64C829992D54AEEE006B9E89 /* libffi.a */, 64C829982D54AEED006B9E89 /* libgmp.a */, 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */, - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-3esYZFBUokREq84LvcOgzJ-ghc9.6.3.a */, - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-3esYZFBUokREq84LvcOgzJ.a */, + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-IckAKQLBKZZ3c4EBa1qhzo-ghc9.6.3.a */, + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-IckAKQLBKZZ3c4EBa1qhzo.a */, ); path = Libraries; sourceTree = ""; diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index b2a9611593..d95e5233c1 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -4663,6 +4663,7 @@ public enum Format: Decodable, Equatable, Hashable { case strikeThrough case snippet case secret + case small case colored(color: FormatColor) case uri case hyperLink(showText: String?, linkUri: String) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 668e18cf6c..3d6b227df7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -4363,6 +4363,7 @@ sealed class Format { @Serializable @SerialName("strikeThrough") class StrikeThrough: Format() @Serializable @SerialName("snippet") class Snippet: Format() @Serializable @SerialName("secret") class Secret: Format() + @Serializable @SerialName("small") class Small: Format() @Serializable @SerialName("colored") class Colored(val color: FormatColor): Format() @Serializable @SerialName("uri") class Uri: Format() @Serializable @SerialName("hyperLink") class HyperLink(val showText: String?, val linkUri: String): Format() @@ -4384,6 +4385,7 @@ sealed class Format { is StrikeThrough -> SpanStyle(textDecoration = TextDecoration.LineThrough) is Snippet -> SpanStyle(fontFamily = FontFamily.Monospace) is Secret -> SpanStyle(color = Color.Transparent, background = SecretColor) + is Small -> SpanStyle(fontSize = MaterialTheme.typography.body2.fontSize, color = MaterialTheme.colors.secondary) is Colored -> SpanStyle(color = this.color.uiColor) is Uri -> linkStyle is HyperLink -> linkStyle diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt index 60595fc255..3984e5bc40 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt @@ -153,6 +153,7 @@ fun MarkdownText ( is Format.Italic -> withStyle(ft.format.style) { append(ft.text) } is Format.StrikeThrough -> withStyle(ft.format.style) { append(ft.text) } is Format.Snippet -> withStyle(ft.format.style) { append(ft.text) } + is Format.Small -> withStyle(ft.format.style) { append(ft.text) } is Format.Colored -> withStyle(ft.format.style) { append(ft.text) } is Format.Secret -> { val ftStyle = ft.format.style diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index 5dcfe81831..abb26bf266 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -1987,6 +1987,9 @@ Snippet: Secret: - type: "secret" +Small: +- type: "small" + Colored: - type: "colored" - color: [Color](#color) diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index 65ce90f647..0e9a527f1a 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -2281,6 +2281,7 @@ export type Format = | Format.StrikeThrough | Format.Snippet | Format.Secret + | Format.Small | Format.Colored | Format.Uri | Format.HyperLink @@ -2297,6 +2298,7 @@ export namespace Format { | "strikeThrough" | "snippet" | "secret" + | "small" | "colored" | "uri" | "hyperLink" @@ -2330,6 +2332,10 @@ export namespace Format { type: "secret" } + export interface Small extends Interface { + type: "small" + } + export interface Colored extends Interface { type: "colored" color: Color diff --git a/plans/2026-03-21-text-size-markdown.md b/plans/2026-03-21-text-size-markdown.md new file mode 100644 index 0000000000..15389a2c1b --- /dev/null +++ b/plans/2026-03-21-text-size-markdown.md @@ -0,0 +1,66 @@ +# Small Text Markdown + +Add `!- text!` syntax for small gray text — legal disclaimers, secondary commentary, LLM reasoning, etc. + +## Syntax + +`!- text!` — renders as small gray text. Uses the `!` style prefix family, `-` for "reduced." + +On old clients: `!- fine print!` shows as-is (old `coloredP` fails on `-`, falls to `wordP`). Readable. + +## Changes + +### Haskell — `src/Simplex/Chat/Markdown.hs` + +1. **`Format`**: add `Small` constructor (no fields). + +2. **`coloredP` parser**: before trying `colorP`, check for `-` followed by space. If matched, produce `Small`. Otherwise fall through to existing color parsing. + +3. **`markdownText`**: add `Small` case, reconstruct as `!- text!`. + +4. **JSON serialization**: TH-derived `ToJSON`/`FromJSON` via existing `sumTypeJSON fstToLower`. Produces `{"small": {}}`. Old Haskell `FromJSON Format` falls to `Unknown` via `<|> pure (Unknown v)`. + +### Haskell — `src/Simplex/Chat/Styled.hs` + +5. **`sgr`**: add `Small` case — map to `FaintIntensity` for terminal rendering. + +### Haskell — `tests/MarkdownTests.hs` + +6. Tests for: + - `!- text!` parses as `Small` + - `!- text!` with leading/trailing spaces in content → no format (same rule as other formats) + - Existing color syntax unchanged + - `markdownText` round-trip + +### iOS — `apps/ios/SimpleXChat/ChatTypes.swift` + +7. **`Format`** enum: add `case small`. + +### iOS — `apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift` + +8. **`messageText`**: render `Small` with smaller `UIFont` point size + gray color. + +### Android — `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt` + +9. **`Format`**: add `@Serializable @SerialName("small") class Small: Format()`. +10. **`Format.style`**: `SpanStyle` with smaller font size + gray color. + +### Android — `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt` + +11. **`MarkdownText`**: add `is Format.Small` case — same pattern as `Bold`/`Italic` (apply style, append text). + +## Backward Compatibility + +### Local (old app receiving message with new syntax) +- Old app's bundled Haskell parses raw message text. Old `coloredP` doesn't know `-`, fails, falls to `wordP`. Text shows as `!- fine print!` — plain text with delimiters. + +### Remote desktop (old desktop, new mobile) +- New mobile Haskell parses `!- text!` as `Small`, serializes to JSON `{"small": {}}`. +- Old desktop Haskell re-parses JSON via `J.parseJSON` (`Remote/Protocol.hs:184`). Old `FromJSON Format` doesn't know `"small"` → `<|> pure (Unknown v)`. +- `Unknown` re-serializes to `{"type": "unknown", "json": ...}` → Kotlin `Format.Unknown` (`ignoreUnknownKeys` drops extra fields). Text renders without formatting. + +## Order of Implementation + +1. Haskell types + parser + tests +2. iOS types + rendering +3. Android types + rendering diff --git a/src/Simplex/Chat/Markdown.hs b/src/Simplex/Chat/Markdown.hs index 3287b263d4..86ba441a57 100644 --- a/src/Simplex/Chat/Markdown.hs +++ b/src/Simplex/Chat/Markdown.hs @@ -53,6 +53,7 @@ data Format | StrikeThrough | Snippet | Secret + | Small | Colored {color :: FormatColor} | Uri -- showText is Nothing for the usual Uri without text @@ -202,7 +203,7 @@ markdownP = mconcat <$> A.many' fragmentP '~' -> formattedP '~' StrikeThrough '`' -> formattedP '`' Snippet '#' -> A.char '#' *> secretP - '!' -> coloredP <|> wordP + '!' -> styledP <|> wordP '@' -> mentionP <|> wordP '/' -> commandP <|> wordP '[' -> sowLinkP <|> wordP @@ -228,13 +229,13 @@ markdownP = mconcat <$> A.many' fragmentP | otherwise = markdown Secret $ T.init ss where ss = b <> s <> a - coloredP :: Parser Markdown - coloredP = do - clr <- A.char '!' *> colorP <* A.space + styledP :: Parser Markdown + styledP = do + f <- A.char '!' *> ((A.char '-' $> Small) <|> (colored <$> colorP)) <* A.space s <- ((<>) <$> A.takeWhile1 (\c -> c /= ' ' && c /= '!') <*> A.takeTill (== '!')) <* A.char '!' if T.null s || T.last s == ' ' - then fail "not colored" - else pure $ markdown (colored clr) s + then fail "not styled" + else pure $ markdown f s mentionP = prefixedStringP '@' displayNameTextP_ Mention commandP = prefixedStringP '/' commandTextP Command prefixedStringP pfx parser format = do @@ -441,6 +442,7 @@ markdownText (FormattedText f_ t) = case f_ of StrikeThrough -> around '~' Snippet -> around '`' Secret -> around '#' + Small -> "!- " <> t <> "!" Colored (FormatColor c) -> color c Uri -> t HyperLink {} -> t diff --git a/src/Simplex/Chat/Styled.hs b/src/Simplex/Chat/Styled.hs index 27c7936009..2dd1faa391 100644 --- a/src/Simplex/Chat/Styled.hs +++ b/src/Simplex/Chat/Styled.hs @@ -75,6 +75,7 @@ sgr = \case StrikeThrough -> [SetSwapForegroundBackground True] Colored (FormatColor c) -> [SetColor Foreground Vivid c] Secret -> [SetColor Foreground Dull Black, SetColor Background Dull Black] + Small -> [SetConsoleIntensity FaintIntensity] _ -> [] unStyle :: StyledString -> String diff --git a/tests/MarkdownTests.hs b/tests/MarkdownTests.hs index ecf923901f..3683b536dd 100644 --- a/tests/MarkdownTests.hs +++ b/tests/MarkdownTests.hs @@ -20,6 +20,7 @@ markdownTests :: Spec markdownTests = do textFormat secretText + textSmall textColor textWithUri textWithHyperlink @@ -131,6 +132,35 @@ secretText = describe "secret text" do "snippet: `this is #secret_text#`" <==> "snippet: " <> markdown Snippet "this is #secret_text#" +small :: Text -> Markdown +small = markdown Small + +textSmall :: Spec +textSmall = describe "text small" do + it "correct markdown" do + "this is !- small! text" + <==> "this is " <> small "small" <> " text" + "!- small! text" + <==> small "small" <> " text" + "this is !- small!" + <==> "this is " <> small "small" + " !- small! text" + <==> " " <> small "small" <> " text" + "this is !- small! " + <==> "this is " <> small "small" <> " " + it "ignored as markdown" do + "this is !- unformatted ! text" + <==> "this is !- unformatted ! text" + "this is !- unformatted! text" + <==> "this is !- unformatted! text" + "this is!- unformatted! text" + <==> "this is!- unformatted! text" + "this is !- unformatted text" + <==> "this is !- unformatted text" + it "ignored internal markdown" do + "this is !- long *small* (not bold)! text" + <==> "this is " <> small "long *small* (not bold)" <> " text" + red :: Text -> Markdown red = markdown (colored Red) diff --git a/website/src/directory.html b/website/src/directory.html index 0235583ece..b20e279d82 100644 --- a/website/src/directory.html +++ b/website/src/directory.html @@ -94,6 +94,15 @@ active_directory: true color: magenta; } + #directory .entry .small-text { + font-size: 0.8em; + color: #888; + } + + .dark #directory .entry .small-text { + color: #999; + } + .dark #directory .entry .green { color: #4DDA67; } diff --git a/website/src/js/directory.js b/website/src/js/directory.js index 6ccdf23033..3000ed0877 100644 --- a/website/src/js/directory.js +++ b/website/src/js/directory.js @@ -433,6 +433,9 @@ function renderMarkdown(fts) { case 'secret': html += `${escapeHtml(text)}`; break; + case 'small': + html += `${escapeHtml(text)}`; + break; case 'colored': html += `${escapeHtml(text)}`; break;