mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-01 22:18:43 +00:00
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 <evgeny@poberezkin.com>
This commit is contained in:
co-authored by
Evgeny Poberezkin
parent
2472bee6b6
commit
a8a8883027
@@ -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)
|
||||
|
||||
@@ -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 = "<group>"; };
|
||||
64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-3esYZFBUokREq84LvcOgzJ.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.9-3esYZFBUokREq84LvcOgzJ.a"; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.9-IckAKQLBKZZ3c4EBa1qhzo.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.9-IckAKQLBKZZ3c4EBa1qhzo.a"; sourceTree = "<group>"; };
|
||||
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = "<group>"; };
|
||||
64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = "<group>"; };
|
||||
@@ -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 = "<group>";
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
@@ -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
|
||||
|
||||
@@ -1987,6 +1987,9 @@ Snippet:
|
||||
Secret:
|
||||
- type: "secret"
|
||||
|
||||
Small:
|
||||
- type: "small"
|
||||
|
||||
Colored:
|
||||
- type: "colored"
|
||||
- color: [Color](#color)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -433,6 +433,9 @@ function renderMarkdown(fts) {
|
||||
case 'secret':
|
||||
html += `<span class="secret">${escapeHtml(text)}</span>`;
|
||||
break;
|
||||
case 'small':
|
||||
html += `<span class="small-text">${escapeHtml(text)}</span>`;
|
||||
break;
|
||||
case 'colored':
|
||||
html += `<span class="${format.color}">${escapeHtml(text)}</span>`;
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user