diff --git a/README.md b/README.md
index 82e42d581..f4b5cb021 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@
-
+
diff --git a/README_ES.md b/README_ES.md
index 07bb42cd7..4ebb06f2c 100644
--- a/README_ES.md
+++ b/README_ES.md
@@ -15,7 +15,7 @@
-
+
diff --git a/README_KOR.md b/README_KOR.md
index b87667ee2..bfe419d24 100644
--- a/README_KOR.md
+++ b/README_KOR.md
@@ -15,7 +15,7 @@
-
+
diff --git a/README_RU.md b/README_RU.md
index c72d01e9d..7001fcb89 100644
--- a/README_RU.md
+++ b/README_RU.md
@@ -15,7 +15,7 @@
-
+
diff --git a/README_TR.md b/README_TR.md
index 120b7a89d..e4db94627 100644
--- a/README_TR.md
+++ b/README_TR.md
@@ -15,7 +15,7 @@
-
+
diff --git a/README_UR.md b/README_UR.md
index dc7fd1f4f..ce3876a4d 100644
--- a/README_UR.md
+++ b/README_UR.md
@@ -15,7 +15,7 @@
-
+
diff --git a/data/src/main/java/org/monogram/data/datasource/remote/LinkRemoteDataSource.kt b/data/src/main/java/org/monogram/data/datasource/remote/LinkRemoteDataSource.kt
index 931f32902..5a2b70603 100644
--- a/data/src/main/java/org/monogram/data/datasource/remote/LinkRemoteDataSource.kt
+++ b/data/src/main/java/org/monogram/data/datasource/remote/LinkRemoteDataSource.kt
@@ -8,6 +8,7 @@ interface LinkRemoteDataSource {
suspend fun searchPublicChat(username: String): TdApi.Chat?
suspend fun checkChatInviteLink(inviteLink: String): TdApi.ChatInviteLinkInfo?
suspend fun joinChatByInviteLink(inviteLink: String): TdApi.ChatJoinResult?
+ suspend fun getGuardBotWebAppUrl(queryId: Long): TdApi.WebAppUrl?
suspend fun getMe(): TdApi.User?
suspend fun createPrivateChat(userId: Long): TdApi.Chat?
suspend fun searchUserByPhoneNumber(phoneNumber: String): TdApi.User?
diff --git a/data/src/main/java/org/monogram/data/datasource/remote/MessageRemoteDataSource.kt b/data/src/main/java/org/monogram/data/datasource/remote/MessageRemoteDataSource.kt
index d87400941..686811a4c 100644
--- a/data/src/main/java/org/monogram/data/datasource/remote/MessageRemoteDataSource.kt
+++ b/data/src/main/java/org/monogram/data/datasource/remote/MessageRemoteDataSource.kt
@@ -19,6 +19,7 @@ import org.monogram.domain.models.webapp.WebAppInfoModel
import org.monogram.domain.repository.ChecklistDraft
import org.monogram.domain.repository.OlderMessagesPage
import org.monogram.domain.repository.ReadUpdate
+import org.monogram.domain.repository.RichTextParseMode
import org.monogram.domain.repository.SearchChatMessagesResult
interface MessageRemoteDataSource : DraftLinkPreviewRemoteDataSource {
@@ -83,7 +84,8 @@ interface MessageRemoteDataSource : DraftLinkPreviewRemoteDataSource {
threadId: Long?,
sendOptions: MessageSendOptions,
isRtl: Boolean?,
- detectAutomaticBlocks: Boolean
+ detectAutomaticBlocks: Boolean,
+ parseMode: RichTextParseMode = RichTextParseMode.Markdown
): TdApi.Message?
suspend fun sendPhoto(
@@ -191,7 +193,8 @@ interface MessageRemoteDataSource : DraftLinkPreviewRemoteDataSource {
messageId: Long,
markdown: String,
isRtl: Boolean?,
- detectAutomaticBlocks: Boolean
+ detectAutomaticBlocks: Boolean,
+ parseMode: RichTextParseMode = RichTextParseMode.Markdown
): TdApi.Message?
suspend fun editMessageCaption(chatId: Long, messageId: Long, caption: String, entities: List): TdApi.Message?
suspend fun getFullRichMessage(chatId: Long, messageId: Long): TdApi.RichMessage?
diff --git a/data/src/main/java/org/monogram/data/datasource/remote/TdLinkRemoteDataSource.kt b/data/src/main/java/org/monogram/data/datasource/remote/TdLinkRemoteDataSource.kt
index 54e49a165..cc5f1683b 100644
--- a/data/src/main/java/org/monogram/data/datasource/remote/TdLinkRemoteDataSource.kt
+++ b/data/src/main/java/org/monogram/data/datasource/remote/TdLinkRemoteDataSource.kt
@@ -23,6 +23,16 @@ class TdLinkRemoteDataSource(
override suspend fun joinChatByInviteLink(inviteLink: String): TdApi.ChatJoinResult? =
coRunCatching { gateway.execute(TdApi.JoinChatByInviteLink(inviteLink)) }.getOrNull()
+ override suspend fun getGuardBotWebAppUrl(queryId: Long): TdApi.WebAppUrl? =
+ coRunCatching {
+ gateway.execute(
+ TdApi.GetGuardBotWebAppUrl(
+ queryId,
+ buildDefaultWebAppOpenParameters(theme = null)
+ )
+ )
+ }.getOrNull()
+
override suspend fun getMe(): TdApi.User? =
coRunCatching { gateway.execute(TdApi.GetMe()) }.getOrNull()
diff --git a/data/src/main/java/org/monogram/data/datasource/remote/TdMessageRemoteDataSource.kt b/data/src/main/java/org/monogram/data/datasource/remote/TdMessageRemoteDataSource.kt
index 500137e11..68e5fd478 100644
--- a/data/src/main/java/org/monogram/data/datasource/remote/TdMessageRemoteDataSource.kt
+++ b/data/src/main/java/org/monogram/data/datasource/remote/TdMessageRemoteDataSource.kt
@@ -27,7 +27,12 @@ import org.monogram.data.compat.buildInputDocument
import org.monogram.data.compat.buildInputPhoto
import org.monogram.data.compat.buildInputPollOption
import org.monogram.data.compat.buildInputPollTypeQuiz
+import org.monogram.data.compat.buildInputSticker
import org.monogram.data.compat.buildInputVideo
+import org.monogram.data.compat.buildInputVideoNote
+import org.monogram.data.compat.buildInputVoiceNote
+import org.monogram.data.compat.buildRichMessageSourceHtml
+import org.monogram.data.compat.buildRichMessageSourceMarkdown
import org.monogram.data.compat.extractTextDraft
import org.monogram.data.gateway.TdLibException
import org.monogram.data.gateway.TelegramGateway
@@ -35,7 +40,6 @@ import org.monogram.data.infra.FileDownloadQueue
import org.monogram.data.infra.FileUpdateHandler
import org.monogram.data.mapper.MessageMapper
import org.monogram.data.mapper.WebPageMapper
-import org.monogram.data.mapper.toApi
import org.monogram.data.repository.DraftLinkPreviewResolver
import org.monogram.domain.models.DraftLinkPreview
import org.monogram.domain.models.DraftLinkPreviewRequest
@@ -59,6 +63,7 @@ import org.monogram.domain.repository.ChecklistDraft
import org.monogram.domain.repository.OlderMessagesPage
import org.monogram.domain.repository.PollRepository
import org.monogram.domain.repository.ReadUpdate
+import org.monogram.domain.repository.RichTextParseMode
import org.monogram.domain.repository.SearchChatMessagesResult
import org.monogram.domain.repository.UserRepository
import java.util.concurrent.ConcurrentHashMap
@@ -827,10 +832,12 @@ class TdMessageRemoteDataSource(
threadId: Long?,
sendOptions: MessageSendOptions,
isRtl: Boolean?,
- detectAutomaticBlocks: Boolean
+ detectAutomaticBlocks: Boolean,
+ parseMode: RichTextParseMode
): TdApi.Message? {
val content = buildInputMessageRichMessage(
markdown = markdown,
+ parseMode = parseMode,
isRtl = isRtl ?: shouldRenderRtl(markdown),
detectAutomaticBlocks = detectAutomaticBlocks,
clearDraft = true
@@ -1013,9 +1020,8 @@ class TdMessageRemoteDataSource(
override suspend fun sendSticker(chatId: Long, stickerPath: String, replyToMsgId: Long?, threadId: Long?): TdApi.Message? {
val content = TdApi.InputMessageSticker().apply {
- this.sticker = TdApi.InputFileLocal(stickerPath)
- this.width = 512
- this.height = 512
+ sticker = buildInputSticker(TdApi.InputFileLocal(stickerPath), 512, 512)
+ emoji = ""
}
val replyTo = if (replyToMsgId != null && replyToMsgId != 0L) TdApi.InputMessageReplyToMessage(replyToMsgId, null, 0, "") else null
val topicId = resolveTopicId(chatId, threadId)
@@ -1161,9 +1167,7 @@ class TdMessageRemoteDataSource(
threadId: Long?
): TdApi.Message? {
val content = TdApi.InputMessageVideoNote().apply {
- this.videoNote = TdApi.InputFileLocal(videoPath)
- this.duration = duration
- this.length = length
+ videoNote = buildInputVideoNote(TdApi.InputFileLocal(videoPath), duration, length)
}
val replyTo = if (replyToMsgId != null && replyToMsgId != 0L) TdApi.InputMessageReplyToMessage(replyToMsgId, null, 0, "") else null
val topicId = resolveTopicId(chatId, threadId)
@@ -1187,9 +1191,7 @@ class TdMessageRemoteDataSource(
threadId: Long?
): TdApi.Message? {
val content = TdApi.InputMessageVoiceNote().apply {
- this.voiceNote = TdApi.InputFileLocal(voicePath)
- this.duration = duration
- this.waveform = waveform
+ voiceNote = buildInputVoiceNote(TdApi.InputFileLocal(voicePath), duration, waveform)
}
val replyTo = if (replyToMsgId != null && replyToMsgId != 0L) TdApi.InputMessageReplyToMessage(replyToMsgId, null, 0, "") else null
val topicId = resolveTopicId(chatId, threadId)
@@ -1259,10 +1261,12 @@ class TdMessageRemoteDataSource(
messageId: Long,
markdown: String,
isRtl: Boolean?,
- detectAutomaticBlocks: Boolean
+ detectAutomaticBlocks: Boolean,
+ parseMode: RichTextParseMode
): TdApi.Message? {
val content = buildInputMessageRichMessage(
markdown = markdown,
+ parseMode = parseMode,
isRtl = isRtl ?: shouldRenderRtl(markdown),
detectAutomaticBlocks = detectAutomaticBlocks,
clearDraft = false
@@ -1352,16 +1356,17 @@ class TdMessageRemoteDataSource(
private fun buildInputMessageRichMessage(
markdown: String,
+ parseMode: RichTextParseMode,
isRtl: Boolean,
detectAutomaticBlocks: Boolean,
clearDraft: Boolean
): TdApi.InputMessageRichMessage {
+ val source = when (parseMode) {
+ RichTextParseMode.Markdown -> buildRichMessageSourceMarkdown(markdown)
+ RichTextParseMode.Html -> buildRichMessageSourceHtml(markdown)
+ }
return TdApi.InputMessageRichMessage(
- TdApi.InputRichMessage(
- TdApi.RichMessageSourceMarkdown(markdown),
- isRtl,
- detectAutomaticBlocks
- ),
+ TdApi.InputRichMessage(source, isRtl, detectAutomaticBlocks),
clearDraft
)
}
@@ -1427,11 +1432,14 @@ class TdMessageRemoteDataSource(
is MessageEntityType.Mention -> TdApi.TextEntityTypeMention()
is MessageEntityType.TextMention -> TdApi.TextEntityTypeMentionName(value.userId)
is MessageEntityType.Hashtag -> TdApi.TextEntityTypeHashtag()
+ is MessageEntityType.Cashtag -> TdApi.TextEntityTypeCashtag()
is MessageEntityType.BotCommand -> TdApi.TextEntityTypeBotCommand()
is MessageEntityType.Url -> TdApi.TextEntityTypeUrl()
is MessageEntityType.Email -> TdApi.TextEntityTypeEmailAddress()
is MessageEntityType.PhoneNumber -> TdApi.TextEntityTypePhoneNumber()
is MessageEntityType.BankCardNumber -> TdApi.TextEntityTypeBankCardNumber()
+ is MessageEntityType.DateTime -> TdApi.TextEntityTypeDateTime(value.unixTime, null)
+ is MessageEntityType.MediaTimestamp -> TdApi.TextEntityTypeMediaTimestamp(value.mediaTimestampSeconds)
is MessageEntityType.CustomEmoji -> TdApi.TextEntityTypeCustomEmoji(value.emojiId)
is MessageEntityType.Other -> return null
}
@@ -1580,11 +1588,7 @@ class TdMessageRemoteDataSource(
url: String,
theme: ThemeParams?
): WebAppInfoModel? {
- val parameters = TdApi.WebAppOpenParameters().apply {
- this.applicationName = "android"
- this.mode = TdApi.WebAppOpenModeFullSize()
- this.theme = theme?.toApi()
- }
+ val parameters = buildDefaultWebAppOpenParameters(theme)
val isMenuUrl = url.startsWith("menu://")
val normalizedUrl = if (isMenuUrl) url.removePrefix("menu://") else url
diff --git a/data/src/main/java/org/monogram/data/datasource/remote/WebAppOpenParametersFactory.kt b/data/src/main/java/org/monogram/data/datasource/remote/WebAppOpenParametersFactory.kt
new file mode 100644
index 000000000..d9b2f77f3
--- /dev/null
+++ b/data/src/main/java/org/monogram/data/datasource/remote/WebAppOpenParametersFactory.kt
@@ -0,0 +1,12 @@
+package org.monogram.data.datasource.remote
+
+import org.drinkless.tdlib.TdApi
+import org.monogram.data.mapper.toApi
+import org.monogram.domain.models.webapp.ThemeParams
+
+internal fun buildDefaultWebAppOpenParameters(theme: ThemeParams?): TdApi.WebAppOpenParameters =
+ TdApi.WebAppOpenParameters().apply {
+ applicationName = "android"
+ mode = TdApi.WebAppOpenModeFullSize()
+ this.theme = theme?.toApi()
+ }
diff --git a/data/src/main/java/org/monogram/data/di/dataModule.kt b/data/src/main/java/org/monogram/data/di/dataModule.kt
index c7b14f79f..218b53f14 100644
--- a/data/src/main/java/org/monogram/data/di/dataModule.kt
+++ b/data/src/main/java/org/monogram/data/di/dataModule.kt
@@ -165,6 +165,7 @@ import org.monogram.domain.repository.ProfilePhotoRepository
import org.monogram.domain.repository.ProxyDiagnosticsRepository
import org.monogram.domain.repository.ProxyRepository
import org.monogram.domain.repository.PushDebugRepository
+import org.monogram.domain.repository.RichTextParsingRepository
import org.monogram.domain.repository.SessionRepository
import org.monogram.domain.repository.SponsorRepository
import org.monogram.domain.repository.StickerRepository
@@ -726,6 +727,7 @@ val dataModule = module {
single { get() }
single { get() }
single { get() }
+ single { get() as RichTextParsingRepository }
single { get() }
single { get() }
single { get() }
diff --git a/data/src/main/java/org/monogram/data/mapper/InstantViewMapper.kt b/data/src/main/java/org/monogram/data/mapper/InstantViewMapper.kt
index 32c3d4ef7..cf73284d3 100644
--- a/data/src/main/java/org/monogram/data/mapper/InstantViewMapper.kt
+++ b/data/src/main/java/org/monogram/data/mapper/InstantViewMapper.kt
@@ -1,6 +1,7 @@
package org.monogram.data.mapper
import org.drinkless.tdlib.TdApi
+import org.monogram.domain.models.MessageContent
import org.monogram.domain.models.WebPage
import org.monogram.domain.models.webapp.HorizontalAlignment
import org.monogram.domain.models.webapp.InstantViewModel
@@ -12,6 +13,7 @@ import org.monogram.domain.models.webapp.PageBlockRelatedArticle
import org.monogram.domain.models.webapp.PageBlockTableCell
import org.monogram.domain.models.webapp.RichText
import org.monogram.domain.models.webapp.VerticalAlignment
+import org.monogram.domain.models.webapp.toEditorMarkdown
fun map(iv: TdApi.WebPageInstantView, url: String): InstantViewModel {
return iv.toInstantViewModel(url)
@@ -21,14 +23,16 @@ fun TdApi.RichMessage.toDomainRichMessage(
chatId: Long,
messageId: Long,
markdownSource: String? = null
-) = org.monogram.domain.models.MessageContent.RichMessage(
- blocks = blocks.orEmpty().map { it.toPageBlock() },
- isRtl = isRtl,
- isFull = isFull,
- chatId = chatId,
- messageId = messageId,
- markdownSource = markdownSource
-)
+) = blocks.orEmpty().map { it.toPageBlock() }.let { pageBlocks ->
+ MessageContent.RichMessage(
+ blocks = pageBlocks,
+ isRtl = isRtl,
+ isFull = isFull,
+ chatId = chatId,
+ messageId = messageId,
+ markdownSource = markdownSource ?: pageBlocks.toEditorMarkdown()
+ )
+}
private fun TdApi.WebPageInstantView.toInstantViewModel(url: String): InstantViewModel {
return InstantViewModel(
@@ -52,7 +56,14 @@ fun TdApi.PageBlock?.toPageBlock(): PageBlock {
is TdApi.PageBlockSectionHeading -> PageBlock.SectionHeading(text.toRichText(), size)
is TdApi.PageBlockKicker -> PageBlock.Kicker(kicker.toRichText())
is TdApi.PageBlockParagraph -> PageBlock.Paragraph(text.toRichText())
- is TdApi.PageBlockPreformatted -> PageBlock.Preformatted(text.toRichText(), language)
+ is TdApi.PageBlockPreformatted -> {
+ val table = if (language.equals("table", ignoreCase = true)) {
+ text.toRichText().toPlainText().toRichMessageTable()
+ } else {
+ null
+ }
+ table ?: PageBlock.Preformatted(text.toRichText(), language)
+ }
is TdApi.PageBlockFooter -> PageBlock.Footer(footer.toRichText())
is TdApi.PageBlockThinking -> PageBlock.Thinking(text.toRichText())
is TdApi.PageBlockDivider -> PageBlock.Divider
@@ -61,20 +72,21 @@ fun TdApi.PageBlock?.toPageBlock(): PageBlock {
is TdApi.PageBlockList -> PageBlock.ListBlock(
items.orEmpty().map { it.toPageBlockListItem() })
is TdApi.PageBlockBlockQuote -> PageBlock.BlockQuote(
- blocks.orEmpty().firstOrNull().toPageBlock().let { block ->
- when (block) {
- is PageBlock.Paragraph -> block.text
- is PageBlock.Preformatted -> block.text
- else -> RichText.Plain("")
- }
- },
- credit.toRichText()
+ pageBlocks = blocks.orEmpty().map { it.toPageBlock() },
+ credit = credit.toRichText()
)
is TdApi.PageBlockPullQuote -> PageBlock.PullQuote(text.toRichText(), credit.toRichText())
is TdApi.PageBlockAnimation -> animation?.let { PageBlock.AnimationBlock(it.toAnimation(), caption.toCaption(), needAutoplay) }
?: PageBlock.Unsupported(this::class.simpleName.orEmpty())
is TdApi.PageBlockAudio -> audio?.let { PageBlock.AudioBlock(it.toAudio(), caption.toCaption()) }
?: PageBlock.Unsupported(this::class.simpleName.orEmpty())
+ is TdApi.PageBlockVoiceNote -> voiceNote?.let {
+ PageBlock.VoiceNoteBlock(
+ it.toVoiceNote(),
+ caption.toCaption()
+ )
+ }
+ ?: PageBlock.Unsupported(this::class.simpleName.orEmpty())
is TdApi.PageBlockPhoto -> photo?.let { PageBlock.PhotoBlock(it.toPhoto(), caption.toCaption(), url) }
?: PageBlock.Unsupported(this::class.simpleName.orEmpty())
is TdApi.PageBlockVideo -> video?.let { PageBlock.VideoBlock(it.toVideo(), caption.toCaption(), needAutoplay, isLooped) }
@@ -176,7 +188,13 @@ fun TdApi.RichText?.toRichText(): RichText = when (this) {
is TdApi.RichTextMathematicalExpression -> RichText.MathematicalExpression(expression)
is TdApi.RichTextReference -> RichText.Reference(text.toRichText(), name, "")
- is TdApi.RichTextReferenceLink -> RichText.Reference(text.toRichText(), referenceName, url)
+ is TdApi.RichTextReferenceLink -> RichText.ReferenceLink(
+ text = text.toRichText(),
+ referenceName = referenceName,
+ url = url
+ )
+
+ is TdApi.RichTextDiff -> RichText.Diff(text.toRichText(), oldText.toRichText())
is TdApi.RichTextAnchor -> RichText.Anchor(name)
is TdApi.RichTextAnchorLink -> RichText.AnchorLink(text.toRichText(), anchorName, url)
is TdApi.RichTexts -> RichText.Texts(texts.orEmpty().map { it.toRichText() })
@@ -235,6 +253,107 @@ private fun TdApi.Photo.toPhoto(): WebPage.Photo {
)
}
+private fun RichText.toPlainText(): String {
+ return when (this) {
+ is RichText.Plain -> text
+ is RichText.Bold -> text.toPlainText()
+ is RichText.Italic -> text.toPlainText()
+ is RichText.Underline -> text.toPlainText()
+ is RichText.Strikethrough -> text.toPlainText()
+ is RichText.Spoiler -> text.toPlainText()
+ is RichText.DateTime -> text.toPlainText()
+ is RichText.Mention -> text.toPlainText()
+ is RichText.Hashtag -> text.toPlainText()
+ is RichText.Cashtag -> text.toPlainText()
+ is RichText.BotCommand -> text.toPlainText()
+ is RichText.Fixed -> text.toPlainText()
+ is RichText.MentionName -> text.toPlainText()
+ is RichText.Url -> text.toPlainText()
+ is RichText.EmailAddress -> text.toPlainText()
+ is RichText.BankCardNumber -> text.toPlainText()
+ is RichText.Subscript -> text.toPlainText()
+ is RichText.Superscript -> text.toPlainText()
+ is RichText.Marked -> text.toPlainText()
+ is RichText.PhoneNumber -> text.toPlainText()
+ is RichText.CustomEmoji -> alternativeText
+ is RichText.Icon -> ""
+ is RichText.MathematicalExpression -> expression
+ is RichText.Reference -> text.toPlainText()
+ is RichText.ReferenceLink -> text.toPlainText()
+ is RichText.Diff -> text.toPlainText()
+ is RichText.Anchor -> ""
+ is RichText.AnchorLink -> text.toPlainText()
+ is RichText.Texts -> texts.joinToString("") { it.toPlainText() }
+ }
+}
+
+private fun String.toRichMessageTable(): PageBlock.Table? {
+ val rows = parseRichMessageTableRows() ?: return null
+ if (rows.size < 2) return null
+
+ return PageBlock.Table(
+ caption = RichText.Plain(""),
+ cells = rows.mapIndexed { rowIndex, row ->
+ row.map { cell ->
+ PageBlockTableCell(
+ text = RichText.Plain(cell),
+ isHeader = rowIndex == 0,
+ colspan = 1,
+ rowspan = 1,
+ align = HorizontalAlignment.LEFT,
+ valign = VerticalAlignment.TOP
+ )
+ }
+ },
+ isBordered = true,
+ isStriped = false
+ )
+}
+
+private fun String.parseRichMessageTableRows(): List>? {
+ val lines = lineSequence()
+ .map { it.trimEnd() }
+ .filter { it.isNotBlank() }
+ .toList()
+ if (lines.isEmpty()) return null
+
+ val boxRows = lines.filter { it.contains('│') }
+ if (boxRows.size >= 2) {
+ return boxRows.mapNotNull { line -> parseDelimitedTableRow(line, '│') }
+ .takeIf { it.size >= 2 && it.allSameSize() }
+ }
+
+ val pipeRows = lines
+ .filterNot { isMarkdownTableSeparatorRow(it) }
+ .filter { it.contains('|') }
+ return pipeRows.mapNotNull { line -> parseDelimitedTableRow(line, '|') }
+ .takeIf { it.size >= 2 && it.allSameSize() }
+}
+
+private fun parseDelimitedTableRow(line: String, delimiter: Char): List? {
+ val parts = line.split(delimiter)
+ .map { it.trim() }
+ val normalized = when {
+ parts.size >= 2 && parts.first().isEmpty() && parts.last().isEmpty() -> parts.drop(1)
+ .dropLast(1)
+
+ else -> parts
+ }
+ if (normalized.size < 2) return null
+ return normalized
+}
+
+private fun List>.allSameSize(): Boolean {
+ val firstSize = firstOrNull()?.size ?: return false
+ return all { it.size == firstSize }
+}
+
+private fun isMarkdownTableSeparatorRow(line: String): Boolean {
+ val compact = line.trim()
+ if (compact.isEmpty()) return false
+ return compact.all { it == '|' || it == ':' || it == '-' || it.isWhitespace() }
+}
+
private fun TdApi.Animation.toAnimation() = WebPage.Animation(
path = animation.local.path.takeIf { isValidFilePath(it) },
width = width,
@@ -254,6 +373,13 @@ private fun TdApi.Audio.toAudio() = WebPage.Audio(
fileId = audio.id
)
+private fun TdApi.VoiceNote.toVoiceNote() = WebPage.VoiceNote(
+ path = voice.local.path.takeIf { isValidFilePath(it) },
+ duration = duration,
+ mimeType = mimeType,
+ fileId = voice.id
+)
+
private fun TdApi.Video.toVideo() = WebPage.Video(
path = video.local.path.takeIf { isValidFilePath(it) },
width = width,
diff --git a/data/src/main/java/org/monogram/data/mapper/TextEntityMapper.kt b/data/src/main/java/org/monogram/data/mapper/TextEntityMapper.kt
index debacc5f5..0b660fca2 100644
--- a/data/src/main/java/org/monogram/data/mapper/TextEntityMapper.kt
+++ b/data/src/main/java/org/monogram/data/mapper/TextEntityMapper.kt
@@ -30,11 +30,14 @@ internal fun TdApi.TextEntity.toMessageEntityOrNull(
}
is TdApi.TextEntityTypeHashtag -> MessageEntityType.Hashtag
+ is TdApi.TextEntityTypeCashtag -> MessageEntityType.Cashtag
is TdApi.TextEntityTypeBotCommand -> MessageEntityType.BotCommand
is TdApi.TextEntityTypeUrl -> MessageEntityType.Url
is TdApi.TextEntityTypeEmailAddress -> MessageEntityType.Email
is TdApi.TextEntityTypePhoneNumber -> MessageEntityType.PhoneNumber
is TdApi.TextEntityTypeBankCardNumber -> MessageEntityType.BankCardNumber
+ is TdApi.TextEntityTypeDateTime -> MessageEntityType.DateTime(entityType.unixTime)
+ is TdApi.TextEntityTypeMediaTimestamp -> MessageEntityType.MediaTimestamp(entityType.mediaTimestamp)
is TdApi.TextEntityTypeBlockQuote -> MessageEntityType.BlockQuote
is TdApi.TextEntityTypeExpandableBlockQuote -> MessageEntityType.BlockQuoteExpandable
is TdApi.TextEntityTypeCustomEmoji -> {
diff --git a/data/src/main/java/org/monogram/data/mapper/message/MessageContentMapper.kt b/data/src/main/java/org/monogram/data/mapper/message/MessageContentMapper.kt
index 2ec040b29..964bacda1 100644
--- a/data/src/main/java/org/monogram/data/mapper/message/MessageContentMapper.kt
+++ b/data/src/main/java/org/monogram/data/mapper/message/MessageContentMapper.kt
@@ -4,6 +4,8 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.drinkless.tdlib.TdApi
import org.monogram.data.chats.ChatCache
+import org.monogram.data.compat.legacyPrizeTonAmount
+import org.monogram.data.compat.legacyStakeTonAmount
import org.monogram.data.datasource.remote.TdMessageRemoteDataSource
import org.monogram.data.mapper.CustomEmojiLoader
import org.monogram.data.mapper.TdFileHelper
@@ -625,12 +627,12 @@ internal class MessageContentMapper(
}
private fun mapStakeDice(content: TdApi.MessageStakeDice): MessageContent {
- val stakeTon = content.stakeToncoinAmount / 1_000_000_000.0
+ val stakeTon = content.legacyStakeTonAmount() / 1_000_000_000.0
val base = if (content.value == 0) {
"Staked dice • $stakeTon TON"
} else {
- val prizeTon = content.prizeToncoinAmount / 1_000_000_000.0
- if (content.prizeToncoinAmount >= 0) {
+ val prizeTon = content.legacyPrizeTonAmount() / 1_000_000_000.0
+ if (content.legacyPrizeTonAmount() >= 0) {
"Staked dice • result ${content.value} • stake $stakeTon TON • prize $prizeTon TON"
} else {
"Staked dice • result ${content.value} • stake $stakeTon TON"
diff --git a/data/src/main/java/org/monogram/data/mapper/message/MessagePersistenceMapper.kt b/data/src/main/java/org/monogram/data/mapper/message/MessagePersistenceMapper.kt
index eab2c0a5d..c324f4410 100644
--- a/data/src/main/java/org/monogram/data/mapper/message/MessagePersistenceMapper.kt
+++ b/data/src/main/java/org/monogram/data/mapper/message/MessagePersistenceMapper.kt
@@ -2,6 +2,8 @@ package org.monogram.data.mapper.message
import org.drinkless.tdlib.TdApi
import org.monogram.data.chats.ChatCache
+import org.monogram.data.compat.legacyPrizeTonAmount
+import org.monogram.data.compat.legacyStakeTonAmount
import org.monogram.data.mapper.SenderNameResolver
import org.monogram.data.mapper.TdFileHelper
import org.monogram.data.mapper.toDomainReplyMarkup
@@ -352,7 +354,11 @@ internal class MessagePersistenceMapper(
is TdApi.MessageStakeDice -> CachedMessageContent(
"stake_dice",
"Staked dice",
- encodeMeta(content.value, content.stakeToncoinAmount, content.prizeToncoinAmount)
+ encodeMeta(
+ content.value,
+ content.legacyStakeTonAmount(),
+ content.legacyPrizeTonAmount()
+ )
)
is TdApi.MessageChecklist -> CachedMessageContent(
@@ -915,7 +921,10 @@ internal class MessagePersistenceMapper(
.joinToString(" ")
}
- is PageBlock.BlockQuote -> text.plainText()
+ is PageBlock.BlockQuote -> listOf(
+ pageBlocks.joinToString("\n") { it.plainText() },
+ credit.plainText()
+ ).filter { it.isNotBlank() }.joinToString("\n")
is PageBlock.PullQuote -> text.plainText()
is PageBlock.Details -> pageBlocks.joinToString("\n") { it.plainText() }
is PageBlock.Table -> cells.joinToString("\n") { row ->
@@ -927,6 +936,7 @@ internal class MessagePersistenceMapper(
is PageBlock.VideoBlock -> caption.text.plainText()
is PageBlock.AnimationBlock -> caption.text.plainText()
is PageBlock.AudioBlock -> caption.text.plainText()
+ is PageBlock.VoiceNoteBlock -> caption.text.plainText()
is PageBlock.Collage -> caption.text.plainText()
is PageBlock.Slideshow -> caption.text.plainText()
is PageBlock.Embedded -> caption.text.plainText()
@@ -966,6 +976,8 @@ internal class MessagePersistenceMapper(
is RichText.Icon -> ""
is RichText.MathematicalExpression -> expression
is RichText.Reference -> text.plainText()
+ is RichText.ReferenceLink -> text.plainText()
+ is RichText.Diff -> text.plainText()
is RichText.Anchor -> ""
is RichText.AnchorLink -> text.plainText()
is RichText.Texts -> texts.joinToString("") { it.plainText() }
@@ -1115,10 +1127,14 @@ internal class MessagePersistenceMapper(
is TdApi.TextEntityTypeMention -> append("m")
is TdApi.TextEntityTypeMentionName -> append("mn,").append(type.userId)
is TdApi.TextEntityTypeHashtag -> append("h")
+ is TdApi.TextEntityTypeCashtag -> append("ch")
is TdApi.TextEntityTypeBotCommand -> append("bc")
is TdApi.TextEntityTypeCustomEmoji -> append("ce,").append(type.customEmojiId)
is TdApi.TextEntityTypeEmailAddress -> append("em")
is TdApi.TextEntityTypePhoneNumber -> append("ph")
+ is TdApi.TextEntityTypeBankCardNumber -> append("bn")
+ is TdApi.TextEntityTypeDateTime -> append("dt,").append(type.unixTime)
+ is TdApi.TextEntityTypeMediaTimestamp -> append("mt,").append(type.mediaTimestamp)
else -> append("?")
}
}
@@ -1147,6 +1163,7 @@ internal class MessagePersistenceMapper(
"m" -> MessageEntityType.Mention
"mn" -> MessageEntityType.TextMention(parts.getOrNull(3)?.toLongOrNull() ?: 0L)
"h" -> MessageEntityType.Hashtag
+ "ch" -> MessageEntityType.Cashtag
"bc" -> MessageEntityType.BotCommand
"ce" -> MessageEntityType.CustomEmoji(
parts.getOrNull(3)?.toLongOrNull() ?: 0L,
@@ -1155,6 +1172,9 @@ internal class MessagePersistenceMapper(
"em" -> MessageEntityType.Email
"ph" -> MessageEntityType.PhoneNumber
+ "bn" -> MessageEntityType.BankCardNumber
+ "dt" -> MessageEntityType.DateTime(parts.getOrNull(3)?.toIntOrNull() ?: 0)
+ "mt" -> MessageEntityType.MediaTimestamp(parts.getOrNull(3)?.toIntOrNull() ?: 0)
else -> null
} ?: return@mapNotNull null
diff --git a/data/src/main/java/org/monogram/data/mapper/message/MessageTdExtensions.kt b/data/src/main/java/org/monogram/data/mapper/message/MessageTdExtensions.kt
index 565ca35fb..f4b06d485 100644
--- a/data/src/main/java/org/monogram/data/mapper/message/MessageTdExtensions.kt
+++ b/data/src/main/java/org/monogram/data/mapper/message/MessageTdExtensions.kt
@@ -1,6 +1,7 @@
package org.monogram.data.mapper.message
import org.drinkless.tdlib.TdApi
+import org.monogram.data.compat.toLegacyToncoinCentCount
import org.monogram.domain.models.FactCheckModel
import org.monogram.domain.models.MessageEntity
import org.monogram.domain.models.MessageEntityType
@@ -17,8 +18,7 @@ internal fun TdApi.FactCheck.toDomain(): FactCheckModel = FactCheckModel(
internal fun TdApi.SuggestedPostInfo.toDomain(): SuggestedPostInfoModel = SuggestedPostInfoModel(
price = when (val value = price) {
is TdApi.SuggestedPostPriceStar -> SuggestedPostPriceModel.Star(value.starCount)
- is TdApi.SuggestedPostPriceTon -> SuggestedPostPriceModel.Ton(value.toncoinCentCount)
- else -> null
+ else -> value?.toLegacyToncoinCentCount()?.let(SuggestedPostPriceModel::Ton)
},
sendDate = sendDate,
state = when (state) {
@@ -46,11 +46,14 @@ internal fun Array?.toDomainEntities(): List {
is TdApi.TextEntityTypeMention -> MessageEntityType.Mention
is TdApi.TextEntityTypeMentionName -> MessageEntityType.TextMention(entityType.userId)
is TdApi.TextEntityTypeHashtag -> MessageEntityType.Hashtag
+ is TdApi.TextEntityTypeCashtag -> MessageEntityType.Cashtag
is TdApi.TextEntityTypeBotCommand -> MessageEntityType.BotCommand
is TdApi.TextEntityTypeUrl -> MessageEntityType.Url
is TdApi.TextEntityTypeEmailAddress -> MessageEntityType.Email
is TdApi.TextEntityTypePhoneNumber -> MessageEntityType.PhoneNumber
is TdApi.TextEntityTypeBankCardNumber -> MessageEntityType.BankCardNumber
+ is TdApi.TextEntityTypeDateTime -> MessageEntityType.DateTime(entityType.unixTime)
+ is TdApi.TextEntityTypeMediaTimestamp -> MessageEntityType.MediaTimestamp(entityType.mediaTimestamp)
is TdApi.TextEntityTypeCustomEmoji -> MessageEntityType.CustomEmoji(entityType.customEmojiId)
is TdApi.TextEntityTypeBlockQuote -> MessageEntityType.BlockQuote
is TdApi.TextEntityTypeExpandableBlockQuote -> MessageEntityType.BlockQuoteExpandable
diff --git a/data/src/main/java/org/monogram/data/mapper/message/ServiceMessageFormatter.kt b/data/src/main/java/org/monogram/data/mapper/message/ServiceMessageFormatter.kt
index e89bebd2f..268572490 100644
--- a/data/src/main/java/org/monogram/data/mapper/message/ServiceMessageFormatter.kt
+++ b/data/src/main/java/org/monogram/data/mapper/message/ServiceMessageFormatter.kt
@@ -2,6 +2,8 @@ package org.monogram.data.mapper.message
import org.drinkless.tdlib.TdApi
import org.monogram.data.chats.ChatCache
+import org.monogram.data.compat.legacyTonAmount
+import org.monogram.data.compat.toLegacyToncoinCentCount
import org.monogram.data.mapper.SenderNameResolver
import org.monogram.domain.models.MessageContent
import org.monogram.domain.models.ServiceEmphasis
@@ -855,7 +857,7 @@ internal class ServiceMessageFormatter(
private fun formatGiftedTon(content: TdApi.MessageGiftedTon): String {
val gifter = content.gifterUserId.takeIf { it != 0L }?.let(::resolveUserName)
val receiver = content.receiverUserId.takeIf { it != 0L }?.let(::resolveUserName)
- val amount = formatTonAmount(content.tonAmount)
+ val amount = formatTonAmount(content.legacyTonAmount())
return when {
gifter != null && receiver != null -> stringProvider.getString(
"service_message_ton_gifted_from_to",
@@ -932,8 +934,11 @@ internal class ServiceMessageFormatter(
val stars = content.starAmount.starCount
return if (stars > 0) {
stringProvider.getString("service_message_suggested_post_paid_stars", stars)
- } else if (content.tonAmount > 0) {
- stringProvider.getString("service_message_suggested_post_paid_ton", formatTonAmount(content.tonAmount))
+ } else if (content.legacyTonAmount() > 0) {
+ stringProvider.getString(
+ "service_message_suggested_post_paid_ton",
+ formatTonAmount(content.legacyTonAmount())
+ )
} else {
stringProvider.getString("service_message_suggested_post_paid")
}
@@ -1013,16 +1018,18 @@ internal class ServiceMessageFormatter(
private fun formatSuggestedPrice(price: TdApi.SuggestedPostPrice): String {
return when (price) {
is TdApi.SuggestedPostPriceStar -> stringProvider.getString("service_message_stars_format", price.starCount)
- is TdApi.SuggestedPostPriceTon -> stringProvider.getString("service_message_ton_format", price.toncoinCentCount / 100.0)
- else -> stringProvider.getString("service_message_unknown_price")
+ else -> price.toLegacyToncoinCentCount()
+ ?.let { stringProvider.getString("service_message_ton_format", it / 100.0) }
+ ?: stringProvider.getString("service_message_unknown_price")
}
}
private fun formatGiftResalePrice(price: TdApi.GiftResalePrice): String {
return when (price) {
is TdApi.GiftResalePriceStar -> stringProvider.getString("service_message_stars_format", price.starCount)
- is TdApi.GiftResalePriceTon -> stringProvider.getString("service_message_ton_format", price.toncoinCentCount / 100.0)
- else -> stringProvider.getString("service_message_unknown_price")
+ else -> price.toLegacyToncoinCentCount()
+ ?.let { stringProvider.getString("service_message_ton_format", it / 100.0) }
+ ?: stringProvider.getString("service_message_unknown_price")
}
}
diff --git a/data/src/main/java/org/monogram/data/mapper/user/EntityEncodingUtils.kt b/data/src/main/java/org/monogram/data/mapper/user/EntityEncodingUtils.kt
index e750e83b6..1e2de51fa 100644
--- a/data/src/main/java/org/monogram/data/mapper/user/EntityEncodingUtils.kt
+++ b/data/src/main/java/org/monogram/data/mapper/user/EntityEncodingUtils.kt
@@ -1,6 +1,7 @@
package org.monogram.data.mapper.user
import org.drinkless.tdlib.TdApi
+import org.monogram.data.compat.buildBotCommand
import org.monogram.domain.models.*
internal fun encodeChatAdministratorRights(rights: TdApi.ChatAdministratorRights?): String? {
@@ -153,7 +154,7 @@ internal fun decodeBotInfoCommands(data: String?): Array {
if (commandSeparator < 0) return@mapNotNull null
val command = item.substring(0, commandSeparator).unescapeStorage()
val description = item.substring(commandSeparator + 1).unescapeStorage()
- TdApi.BotCommand(command, description)
+ buildBotCommand(command, description)
}.toTypedArray()
}
diff --git a/data/src/main/java/org/monogram/data/repository/LinkHandlerRepositoryImpl.kt b/data/src/main/java/org/monogram/data/repository/LinkHandlerRepositoryImpl.kt
index c412cd365..c7925de8e 100644
--- a/data/src/main/java/org/monogram/data/repository/LinkHandlerRepositoryImpl.kt
+++ b/data/src/main/java/org/monogram/data/repository/LinkHandlerRepositoryImpl.kt
@@ -206,14 +206,21 @@ class LinkHandlerRepositoryImpl(
}
}
- private fun mapJoinResult(result: TdApi.ChatJoinResult): LinkAction = when (result) {
+ private suspend fun mapJoinResult(result: TdApi.ChatJoinResult): LinkAction = when (result) {
is TdApi.ChatJoinResultSuccess -> LinkAction.OpenChat(result.chatId)
is TdApi.ChatJoinResultRequestSent -> LinkAction.JoinChatRequestSent()
- is TdApi.ChatJoinResultGuardBotApprovalRequired -> LinkAction.JoinChatApprovalRequired(
- chatId = 0L,
- url = result.url.url,
- queryId = result.queryId
- )
+ is TdApi.ChatJoinResultGuardBotApprovalRequired -> {
+ val url = remote.getGuardBotWebAppUrl(result.queryId)?.url
+ if (url.isNullOrBlank()) {
+ LinkAction.None
+ } else {
+ LinkAction.JoinChatApprovalRequired(
+ chatId = 0L,
+ url = url,
+ queryId = result.queryId
+ )
+ }
+ }
is TdApi.ChatJoinResultDeclined -> LinkAction.JoinChatDeclined
else -> LinkAction.None
diff --git a/data/src/main/java/org/monogram/data/repository/MessageRepositoryImpl.kt b/data/src/main/java/org/monogram/data/repository/MessageRepositoryImpl.kt
index 5edc6bd41..188fb67fd 100644
--- a/data/src/main/java/org/monogram/data/repository/MessageRepositoryImpl.kt
+++ b/data/src/main/java/org/monogram/data/repository/MessageRepositoryImpl.kt
@@ -23,6 +23,8 @@ import org.monogram.data.BuildConfig
import org.monogram.data.chats.ChatCache
import org.monogram.data.compat.buildDraftMessageTextContent
import org.monogram.data.compat.buildTdChatPermissions
+import org.monogram.data.compat.generateTextWithAi
+import org.monogram.data.compat.isPromptBasedAiSupported
import org.monogram.data.core.coRunCatching
import org.monogram.data.datasource.FileDataSource
import org.monogram.data.datasource.cache.ChatLocalDataSource
@@ -74,6 +76,8 @@ import org.monogram.domain.repository.MessageRepository
import org.monogram.domain.repository.MessageThreadContext
import org.monogram.domain.repository.OlderMessagesPage
import org.monogram.domain.repository.ProfileMediaFilter
+import org.monogram.domain.repository.RichTextParseMode
+import org.monogram.domain.repository.RichTextParsingRepository
import org.monogram.domain.repository.SearchChatMessagesResult
import org.monogram.domain.repository.TextCompositionStyleModel
import java.io.File
@@ -98,7 +102,7 @@ internal class MessageRepositoryImpl(
private val stickerPathDao: StickerPathDao,
private val keyValueDao: KeyValueDao,
private val textCompositionStyleDao: TextCompositionStyleDao
-) : MessageRepository {
+) : MessageRepository, RichTextParsingRepository {
private data class RichMessageCacheKey(val chatId: Long, val messageId: Long)
private val fixedDraftLinkPreviewFetcher = FixedDraftLinkPreviewFetcher(
@@ -124,6 +128,7 @@ internal class MessageRepositoryImpl(
override val pinnedMessageFlow = messageRemoteDataSource.pinnedMessageFlow
override val mediaUpdateFlow = messageRemoteDataSource.mediaUpdateFlow
override val textCompositionStyles = _textCompositionStyles.asStateFlow()
+ override val supportsPromptBasedAi = isPromptBasedAiSupported()
init {
scope.launch(dispatcherProvider.io) {
@@ -392,7 +397,8 @@ internal class MessageRepositoryImpl(
threadId: Long?,
sendOptions: MessageSendOptions,
isRtl: Boolean?,
- detectAutomaticBlocks: Boolean
+ detectAutomaticBlocks: Boolean,
+ parseMode: RichTextParseMode
) {
messageRemoteDataSource.sendRichMessage(
chatId = chatId,
@@ -401,7 +407,8 @@ internal class MessageRepositoryImpl(
threadId = threadId,
sendOptions = sendOptions,
isRtl = isRtl,
- detectAutomaticBlocks = detectAutomaticBlocks
+ detectAutomaticBlocks = detectAutomaticBlocks,
+ parseMode = parseMode
)
}
@@ -616,14 +623,16 @@ internal class MessageRepositoryImpl(
messageId: Long,
markdown: String,
isRtl: Boolean?,
- detectAutomaticBlocks: Boolean
+ detectAutomaticBlocks: Boolean,
+ parseMode: RichTextParseMode
) {
messageRemoteDataSource.editRichMessage(
chatId = chatId,
messageId = messageId,
markdown = markdown,
isRtl = isRtl,
- detectAutomaticBlocks = detectAutomaticBlocks
+ detectAutomaticBlocks = detectAutomaticBlocks,
+ parseMode = parseMode
)
}
@@ -947,6 +956,25 @@ internal class MessageRepositoryImpl(
}
}
+ override suspend fun parseTextEntities(
+ text: String,
+ mode: RichTextParseMode
+ ): FormattedTextResult = withContext(dispatcherProvider.io) {
+ val result = gateway.execute(
+ TdApi.ParseTextEntities(
+ text,
+ when (mode) {
+ RichTextParseMode.Markdown -> TdApi.TextParseModeMarkdown(2)
+ RichTextParseMode.Html -> TdApi.TextParseModeHTML()
+ }
+ )
+ )
+ FormattedTextResult(
+ text = result.text,
+ entities = result.entities.orEmpty().mapNotNull { it.toDomainMessageEntity() }
+ )
+ }
+
override suspend fun composeTextWithAi(
text: String,
entities: List,
@@ -1022,11 +1050,14 @@ internal class MessageRepositoryImpl(
is MessageEntityType.Mention -> TdApi.TextEntityTypeMention()
is MessageEntityType.TextMention -> TdApi.TextEntityTypeMentionName(value.userId)
is MessageEntityType.Hashtag -> TdApi.TextEntityTypeHashtag()
+ is MessageEntityType.Cashtag -> TdApi.TextEntityTypeCashtag()
is MessageEntityType.BotCommand -> TdApi.TextEntityTypeBotCommand()
is MessageEntityType.Url -> TdApi.TextEntityTypeUrl()
is MessageEntityType.Email -> TdApi.TextEntityTypeEmailAddress()
is MessageEntityType.PhoneNumber -> TdApi.TextEntityTypePhoneNumber()
is MessageEntityType.BankCardNumber -> TdApi.TextEntityTypeBankCardNumber()
+ is MessageEntityType.DateTime -> TdApi.TextEntityTypeDateTime(value.unixTime, null)
+ is MessageEntityType.MediaTimestamp -> TdApi.TextEntityTypeMediaTimestamp(value.mediaTimestampSeconds)
is MessageEntityType.CustomEmoji -> TdApi.TextEntityTypeCustomEmoji(value.emojiId)
is MessageEntityType.Other -> return@mapNotNull null
}
@@ -1051,11 +1082,14 @@ internal class MessageRepositoryImpl(
is TdApi.TextEntityTypeMention -> MessageEntityType.Mention
is TdApi.TextEntityTypeMentionName -> MessageEntityType.TextMention(value.userId)
is TdApi.TextEntityTypeHashtag -> MessageEntityType.Hashtag
+ is TdApi.TextEntityTypeCashtag -> MessageEntityType.Cashtag
is TdApi.TextEntityTypeBotCommand -> MessageEntityType.BotCommand
is TdApi.TextEntityTypeUrl -> MessageEntityType.Url
is TdApi.TextEntityTypeEmailAddress -> MessageEntityType.Email
is TdApi.TextEntityTypePhoneNumber -> MessageEntityType.PhoneNumber
is TdApi.TextEntityTypeBankCardNumber -> MessageEntityType.BankCardNumber
+ is TdApi.TextEntityTypeDateTime -> MessageEntityType.DateTime(value.unixTime)
+ is TdApi.TextEntityTypeMediaTimestamp -> MessageEntityType.MediaTimestamp(value.mediaTimestamp)
is TdApi.TextEntityTypeCustomEmoji -> MessageEntityType.CustomEmoji(value.customEmojiId)
else -> return null
}
@@ -1918,6 +1952,18 @@ internal class MessageRepositoryImpl(
}
}
+ override suspend fun generateTextWithAi(
+ prompt: String,
+ languageCode: String,
+ addEmojis: Boolean
+ ): FormattedTextResult? = withContext(dispatcherProvider.io) {
+ gateway.generateTextWithAi(
+ prompt = prompt,
+ languageCode = languageCode,
+ addEmojis = addEmojis
+ )
+ }
+
private inline fun traceSection(section: String, block: () -> T): T {
Trace.beginSection(section)
return try {
diff --git a/data/src/official/java/org/drinkless/tdlib/TdApi.java b/data/src/official/java/org/drinkless/tdlib/TdApi.java
index ea4be3f19..17213f85e 100644
--- a/data/src/official/java/org/drinkless/tdlib/TdApi.java
+++ b/data/src/official/java/org/drinkless/tdlib/TdApi.java
@@ -21,7 +21,7 @@ public class TdApi {
}
}
- private static final String GIT_COMMIT_HASH = "a17f87c4cff7b90b278d12b91ba0614383aaee82";
+ private static final String GIT_COMMIT_HASH = "022d60202e446ad1287b9fb68e687c8a0760788b";
private TdApi() {
}
@@ -174,6 +174,7 @@ public abstract static class Function extends Object {
CloseWebApp.CONSTRUCTOR,
CommitPendingLiveStoryReactions.CONSTRUCTOR,
CommitPendingPaidMessageReactions.CONSTRUCTOR,
+ ComposeRichMessageWithAi.CONSTRUCTOR,
ComposeTextWithAi.CONSTRUCTOR,
ConfirmBusinessConnectedBot.CONSTRUCTOR,
ConfirmQrCodeAuthentication.CONSTRUCTOR,
@@ -197,6 +198,7 @@ public abstract static class Function extends Object {
CreateNewStickerSet.CONSTRUCTOR,
CreateNewSupergroupChat.CONSTRUCTOR,
CreatePrivateChat.CONSTRUCTOR,
+ CreateRichMessageWithAi.CONSTRUCTOR,
CreateSecretChat.CONSTRUCTOR,
CreateStoryAlbum.CONSTRUCTOR,
CreateSupergroupChat.CONSTRUCTOR,
@@ -228,6 +230,7 @@ public abstract static class Function extends Object {
DeleteDefaultBackground.CONSTRUCTOR,
DeleteDirectMessagesChatTopicHistory.CONSTRUCTOR,
DeleteDirectMessagesChatTopicMessagesByDate.CONSTRUCTOR,
+ DeleteEphemeralMessage.CONSTRUCTOR,
DeleteFile.CONSTRUCTOR,
DeleteForumTopic.CONSTRUCTOR,
DeleteGiftCollection.CONSTRUCTOR,
@@ -273,6 +276,7 @@ public abstract static class Function extends Object {
EditChatInviteLink.CONSTRUCTOR,
EditChatSubscriptionInviteLink.CONSTRUCTOR,
EditCustomLanguagePackInfo.CONSTRUCTOR,
+ EditEphemeralMessage.CONSTRUCTOR,
EditForumTopic.CONSTRUCTOR,
EditInlineMessageCaption.CONSTRUCTOR,
EditInlineMessageLiveLocation.CONSTRUCTOR,
@@ -299,6 +303,7 @@ public abstract static class Function extends Object {
EndGroupCallRecording.CONSTRUCTOR,
EndGroupCallScreenSharing.CONSTRUCTOR,
FinishFileGeneration.CONSTRUCTOR,
+ FixRichMessageWithAi.CONSTRUCTOR,
FixTextWithAi.CONSTRUCTOR,
ForwardMessages.CONSTRUCTOR,
GetAccountTtl.CONSTRUCTOR,
@@ -440,6 +445,8 @@ public abstract static class Function extends Object {
GetGiftUpgradePreview.CONSTRUCTOR,
GetGiftsForCrafting.CONSTRUCTOR,
GetGiveawayInfo.CONSTRUCTOR,
+ GetGramRevenueStatistics.CONSTRUCTOR,
+ GetGramWithdrawalUrl.CONSTRUCTOR,
GetGreetingStickers.CONSTRUCTOR,
GetGrossingWebAppBots.CONSTRUCTOR,
GetGroupCall.CONSTRUCTOR,
@@ -447,6 +454,7 @@ public abstract static class Function extends Object {
GetGroupCallStreamSegment.CONSTRUCTOR,
GetGroupCallStreams.CONSTRUCTOR,
GetGroupsInCommon.CONSTRUCTOR,
+ GetGuardBotWebAppUrl.CONSTRUCTOR,
GetImportedContactCount.CONSTRUCTOR,
GetInactiveSupergroupChats.CONSTRUCTOR,
GetInlineGameHighScores.CONSTRUCTOR,
@@ -600,9 +608,7 @@ public abstract static class Function extends Object {
GetThemedChatEmojiStatuses.CONSTRUCTOR,
GetThemedEmojiStatuses.CONSTRUCTOR,
GetTimeZones.CONSTRUCTOR,
- GetTonRevenueStatistics.CONSTRUCTOR,
GetTonTransactions.CONSTRUCTOR,
- GetTonWithdrawalUrl.CONSTRUCTOR,
GetTopChats.CONSTRUCTOR,
GetTrendingStickerSets.CONSTRUCTOR,
GetUpgradedGift.CONSTRUCTOR,
@@ -818,6 +824,7 @@ public abstract static class Function extends Object {
SendChatAction.CONSTRUCTOR,
SendCustomRequest.CONSTRUCTOR,
SendEmailAddressVerificationCode.CONSTRUCTOR,
+ SendEphemeralMessage.CONSTRUCTOR,
SendGift.CONSTRUCTOR,
SendGiftPurchaseOffer.CONSTRUCTOR,
SendGroupCallMessage.CONSTRUCTOR,
@@ -1047,7 +1054,9 @@ public abstract static class Function extends Object {
TransferBusinessAccountStars.CONSTRUCTOR,
TransferChatOwnership.CONSTRUCTOR,
TransferGift.CONSTRUCTOR,
+ TranslateMessageRichMessage.CONSTRUCTOR,
TranslateMessageText.CONSTRUCTOR,
+ TranslateRichMessage.CONSTRUCTOR,
TranslateText.CONSTRUCTOR,
UnpinAllChatMessages.CONSTRUCTOR,
UnpinAllDirectMessagesChatTopicMessages.CONSTRUCTOR,
@@ -5716,6 +5725,10 @@ public static class BotCommand extends Object {
* Description of the bot command.
*/
public String description;
+ /**
+ * True, if the command must send an ephemeral message instead of a regular one.
+ */
+ public boolean isEphemeral;
/**
* Represents a command supported by a bot.
@@ -5728,16 +5741,18 @@ public BotCommand() {
*
* @param command Text of the bot command.
* @param description Description of the bot command.
+ * @param isEphemeral True, if the command must send an ephemeral message instead of a regular one.
*/
- public BotCommand(String command, String description) {
+ public BotCommand(String command, String description, boolean isEphemeral) {
this.command = command;
this.description = description;
+ this.isEphemeral = isEphemeral;
}
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = -1032140601;
+ public static final int CONSTRUCTOR = -1614592393;
/**
* @return this.CONSTRUCTOR
@@ -11608,7 +11623,7 @@ public static class ChatAdministratorRights extends Object {
*/
public boolean canManageTopics;
/**
- * True, if the administrator can add new administrators with a subset of their own privileges or demote administrators that were directly or indirectly promoted by them.
+ * True, if the administrator can add new administrators with a subset of their own privileges or demote administrators that were directly or indirectly promoted by them; applicable to supergroups and channels only.
*/
public boolean canPromoteMembers;
/**
@@ -11658,7 +11673,7 @@ public ChatAdministratorRights() {
* @param canRestrictMembers True, if the administrator can restrict, ban, or unban chat members or view supergroup statistics.
* @param canPinMessages True, if the administrator can pin messages; applicable to basic groups and supergroups only.
* @param canManageTopics True, if the administrator can create, rename, close, reopen, hide, and unhide forum topics; applicable to forum supergroups only.
- * @param canPromoteMembers True, if the administrator can add new administrators with a subset of their own privileges or demote administrators that were directly or indirectly promoted by them.
+ * @param canPromoteMembers True, if the administrator can add new administrators with a subset of their own privileges or demote administrators that were directly or indirectly promoted by them; applicable to supergroups and channels only.
* @param canManageVideoChats True, if the administrator can manage video chats.
* @param canPostStories True, if the administrator can create new chat stories, or edit and delete posted stories; applicable to supergroups and channels only.
* @param canEditStories True, if the administrator can edit stories posted by other users, post stories to the chat page, pin chat stories, and access story archive; applicable to supergroups and channels only.
@@ -16401,11 +16416,7 @@ public static class ChatJoinResultGuardBotApprovalRequired extends ChatJoinResul
*/
public long botUserId;
/**
- * The URL of the Web App to open.
- */
- public WebAppUrl url;
- /**
- * Unique identifier of the join request, which will be used in updateChatJoinResult.
+ * Unique identifier of the join request, which will be used in getGuardBotWebAppUrl and updateChatJoinResult.
*/
public long queryId;
@@ -16419,19 +16430,17 @@ public ChatJoinResultGuardBotApprovalRequired() {
* An approval from a guard bot through a Web App is required to join the chat.
*
* @param botUserId Identifier of the guard bot.
- * @param url The URL of the Web App to open.
- * @param queryId Unique identifier of the join request, which will be used in updateChatJoinResult.
+ * @param queryId Unique identifier of the join request, which will be used in getGuardBotWebAppUrl and updateChatJoinResult.
*/
- public ChatJoinResultGuardBotApprovalRequired(long botUserId, WebAppUrl url, long queryId) {
+ public ChatJoinResultGuardBotApprovalRequired(long botUserId, long queryId) {
this.botUserId = botUserId;
- this.url = url;
this.queryId = queryId;
}
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = 1110742849;
+ public static final int CONSTRUCTOR = -930056495;
/**
* @return this.CONSTRUCTOR
@@ -16805,7 +16814,7 @@ public int getConstructor() {
}
/**
- * The user is a member of the chat and has some additional privileges. In basic groups, administrators can edit and delete messages sent by others, add new members, ban unprivileged members, and manage video chats. In supergroups and channels, there are more detailed options for administrator privileges.
+ * The user is a member of the chat and has some additional privileges. In basic groups, administrators have all applicable rights. In supergroups and channels, any subset of the rights can be chosen for an administrator.
*/
public static class ChatMemberStatusAdministrator extends ChatMemberStatus {
/**
@@ -16818,13 +16827,13 @@ public static class ChatMemberStatusAdministrator extends ChatMemberStatus {
public ChatAdministratorRights rights;
/**
- * The user is a member of the chat and has some additional privileges. In basic groups, administrators can edit and delete messages sent by others, add new members, ban unprivileged members, and manage video chats. In supergroups and channels, there are more detailed options for administrator privileges.
+ * The user is a member of the chat and has some additional privileges. In basic groups, administrators have all applicable rights. In supergroups and channels, any subset of the rights can be chosen for an administrator.
*/
public ChatMemberStatusAdministrator() {
}
/**
- * The user is a member of the chat and has some additional privileges. In basic groups, administrators can edit and delete messages sent by others, add new members, ban unprivileged members, and manage video chats. In supergroups and channels, there are more detailed options for administrator privileges.
+ * The user is a member of the chat and has some additional privileges. In basic groups, administrators have all applicable rights. In supergroups and channels, any subset of the rights can be chosen for an administrator.
*
* @param canBeEdited True, if the current user can edit the administrator privileges for the called user.
* @param rights Rights of the administrator.
@@ -18385,9 +18394,9 @@ public int getConstructor() {
*/
public static class ChatRevenueTransactions extends Object {
/**
- * The amount of owned Toncoins; in the smallest units of the cryptocurrency.
+ * The amount of owned TON Grams; in the smallest units of the cryptocurrency.
*/
- public long tonAmount;
+ public long gramAmount;
/**
* List of transactions.
*/
@@ -18406,12 +18415,12 @@ public ChatRevenueTransactions() {
/**
* Contains a list of chat revenue transactions.
*
- * @param tonAmount The amount of owned Toncoins; in the smallest units of the cryptocurrency.
+ * @param gramAmount The amount of owned TON Grams; in the smallest units of the cryptocurrency.
* @param transactions List of transactions.
* @param nextOffset The offset for the next request. If empty, then there are no more results.
*/
- public ChatRevenueTransactions(long tonAmount, ChatRevenueTransaction[] transactions, String nextOffset) {
- this.tonAmount = tonAmount;
+ public ChatRevenueTransactions(long gramAmount, ChatRevenueTransaction[] transactions, String nextOffset) {
+ this.gramAmount = gramAmount;
this.transactions = transactions;
this.nextOffset = nextOffset;
}
@@ -18419,7 +18428,7 @@ public ChatRevenueTransactions(long tonAmount, ChatRevenueTransaction[] transact
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = -2017122771;
+ public static final int CONSTRUCTOR = 166743656;
/**
* @return this.CONSTRUCTOR
@@ -20163,6 +20172,355 @@ public int getConstructor() {
}
}
+ /**
+ * Represents a community consisting of supergroup chats, channel chats and chats with bots.
+ */
+ public static class Community extends Object {
+ /**
+ * Community identifier.
+ */
+ public long id;
+ /**
+ * If false, the community is inaccessible, and the only information known about the community is inside this class. Identifier of the community can't be passed to any method.
+ */
+ public boolean haveAccess;
+ /**
+ * Community name.
+ */
+ public String name;
+ /**
+ * Community photo; may be null.
+ */
+ @Nullable public ChatPhotoInfo photo;
+ /**
+ * Point in time (Unix timestamp) when the community was joined, or the point in time when the community was created, in case the user is not a member of any chat in the community.
+ */
+ public int date;
+ /**
+ * Status of the current user in the community.
+ */
+ public CommunityMemberStatus status;
+ /**
+ * Actions that non-administrator community members are allowed to take in the community.
+ */
+ public CommunityPermissions permissions;
+
+ /**
+ * Represents a community consisting of supergroup chats, channel chats and chats with bots.
+ */
+ public Community() {
+ }
+
+ /**
+ * Represents a community consisting of supergroup chats, channel chats and chats with bots.
+ *
+ * @param id Community identifier.
+ * @param haveAccess If false, the community is inaccessible, and the only information known about the community is inside this class. Identifier of the community can't be passed to any method.
+ * @param name Community name.
+ * @param photo Community photo; may be null.
+ * @param date Point in time (Unix timestamp) when the community was joined, or the point in time when the community was created, in case the user is not a member of any chat in the community.
+ * @param status Status of the current user in the community.
+ * @param permissions Actions that non-administrator community members are allowed to take in the community.
+ */
+ public Community(long id, boolean haveAccess, String name, ChatPhotoInfo photo, int date, CommunityMemberStatus status, CommunityPermissions permissions) {
+ this.id = id;
+ this.haveAccess = haveAccess;
+ this.name = name;
+ this.photo = photo;
+ this.date = date;
+ this.status = status;
+ this.permissions = permissions;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = 1684827592;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * Describes rights of the administrator in a community.
+ */
+ public static class CommunityAdministratorRights extends Object {
+ /**
+ * True, if the user is an administrator. Implied by any other privilege.
+ */
+ public boolean canManageCommunity;
+ /**
+ * True, if the administrator can change the community name, photo, and other settings.
+ */
+ public boolean canChangeInfo;
+ /**
+ * True, if the user can change the chats added to the community.
+ */
+ public boolean canEditChatList;
+ /**
+ * True, if the administrator can add new administrators with a subset of their own privileges or demote administrators that were directly or indirectly promoted by them.
+ */
+ public boolean canPromoteMembers;
+ /**
+ * True, if the administrator can ban, or unban community members.
+ */
+ public boolean canBanMembers;
+
+ /**
+ * Describes rights of the administrator in a community.
+ */
+ public CommunityAdministratorRights() {
+ }
+
+ /**
+ * Describes rights of the administrator in a community.
+ *
+ * @param canManageCommunity True, if the user is an administrator. Implied by any other privilege.
+ * @param canChangeInfo True, if the administrator can change the community name, photo, and other settings.
+ * @param canEditChatList True, if the user can change the chats added to the community.
+ * @param canPromoteMembers True, if the administrator can add new administrators with a subset of their own privileges or demote administrators that were directly or indirectly promoted by them.
+ * @param canBanMembers True, if the administrator can ban, or unban community members.
+ */
+ public CommunityAdministratorRights(boolean canManageCommunity, boolean canChangeInfo, boolean canEditChatList, boolean canPromoteMembers, boolean canBanMembers) {
+ this.canManageCommunity = canManageCommunity;
+ this.canChangeInfo = canChangeInfo;
+ this.canEditChatList = canEditChatList;
+ this.canPromoteMembers = canPromoteMembers;
+ this.canBanMembers = canBanMembers;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = -954068218;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * This class is an abstract base class.
+ * Provides information about the status of a member in a community.
+ */
+ public abstract static class CommunityMemberStatus extends Object {
+ /**
+ * Describes possible values returned by getConstructor().
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ CommunityMemberStatusCreator.CONSTRUCTOR,
+ CommunityMemberStatusAdministrator.CONSTRUCTOR,
+ CommunityMemberStatusMember.CONSTRUCTOR,
+ CommunityMemberStatusLeft.CONSTRUCTOR,
+ CommunityMemberStatusBanned.CONSTRUCTOR
+ })
+ public @interface Constructors {}
+
+ /**
+ * @return identifier uniquely determining type of the object.
+ */
+ @Constructors
+ @Override
+ public abstract int getConstructor();
+ /**
+ * Default class constructor.
+ */
+ public CommunityMemberStatus() {
+ }
+ }
+
+ /**
+ * The user is the owner of the community and has all the administrator privileges.
+ */
+ public static class CommunityMemberStatusCreator extends CommunityMemberStatus {
+
+ /**
+ * The user is the owner of the community and has all the administrator privileges.
+ */
+ public CommunityMemberStatusCreator() {
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = 509736488;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * The user is a member of the community and has some additional privileges.
+ */
+ public static class CommunityMemberStatusAdministrator extends CommunityMemberStatus {
+ /**
+ * True, if the current user can edit the administrator privileges for the called user.
+ */
+ public boolean canBeEdited;
+ /**
+ * Rights of the administrator.
+ */
+ public CommunityAdministratorRights rights;
+
+ /**
+ * The user is a member of the community and has some additional privileges.
+ */
+ public CommunityMemberStatusAdministrator() {
+ }
+
+ /**
+ * The user is a member of the community and has some additional privileges.
+ *
+ * @param canBeEdited True, if the current user can edit the administrator privileges for the called user.
+ * @param rights Rights of the administrator.
+ */
+ public CommunityMemberStatusAdministrator(boolean canBeEdited, CommunityAdministratorRights rights) {
+ this.canBeEdited = canBeEdited;
+ this.rights = rights;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = 1328647725;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * The user is a member of the community, without any additional privileges or restrictions.
+ */
+ public static class CommunityMemberStatusMember extends CommunityMemberStatus {
+
+ /**
+ * The user is a member of the community, without any additional privileges or restrictions.
+ */
+ public CommunityMemberStatusMember() {
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = -1433420907;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * The user or the chat is not a community member.
+ */
+ public static class CommunityMemberStatusLeft extends CommunityMemberStatus {
+
+ /**
+ * The user or the chat is not a community member.
+ */
+ public CommunityMemberStatusLeft() {
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = 102726479;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * The user or the chat was banned in the community; implies ban in all chats in the community.
+ */
+ public static class CommunityMemberStatusBanned extends CommunityMemberStatus {
+
+ /**
+ * The user or the chat was banned in the community; implies ban in all chats in the community.
+ */
+ public CommunityMemberStatusBanned() {
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = -496722083;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * Describes actions that a user is allowed to take in a community.
+ */
+ public static class CommunityPermissions extends Object {
+ /**
+ * True, if the user can change the chats added to the community.
+ */
+ public boolean canEditChatList;
+
+ /**
+ * Describes actions that a user is allowed to take in a community.
+ */
+ public CommunityPermissions() {
+ }
+
+ /**
+ * Describes actions that a user is allowed to take in a community.
+ *
+ * @param canEditChatList True, if the user can change the chats added to the community.
+ */
+ public CommunityPermissions(boolean canEditChatList) {
+ this.canEditChatList = canEditChatList;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = -1885888761;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
/**
* Describes an affiliate program that was connected to an affiliate.
*/
@@ -20721,6 +21079,10 @@ public static class CountryInfo extends Object {
* English name of the country.
*/
public String englishName;
+ /**
+ * An emoji for the flag of the country; may be empty if unknown.
+ */
+ public String flagEmoji;
/**
* True, if the country must be hidden from the list of all countries.
*/
@@ -20742,13 +21104,15 @@ public CountryInfo() {
* @param countryCode A two-letter ISO 3166-1 alpha-2 country code.
* @param name Native name of the country.
* @param englishName English name of the country.
+ * @param flagEmoji An emoji for the flag of the country; may be empty if unknown.
* @param isHidden True, if the country must be hidden from the list of all countries.
* @param callingCodes List of country calling codes.
*/
- public CountryInfo(String countryCode, String name, String englishName, boolean isHidden, String[] callingCodes) {
+ public CountryInfo(String countryCode, String name, String englishName, String flagEmoji, boolean isHidden, String[] callingCodes) {
this.countryCode = countryCode;
this.name = name;
this.englishName = englishName;
+ this.flagEmoji = flagEmoji;
this.isHidden = isHidden;
this.callingCodes = callingCodes;
}
@@ -20756,7 +21120,7 @@ public CountryInfo(String countryCode, String name, String englishName, boolean
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = 1617195722;
+ public static final int CONSTRUCTOR = 465611797;
/**
* @return this.CONSTRUCTOR
@@ -27467,13 +27831,13 @@ public static class GiftResaleParameters extends Object {
*/
public long starCount;
/**
- * Resale price of the gift in 1/100 of Toncoin.
+ * Resale price of the gift in 1/100 of TON Gram.
*/
- public long toncoinCentCount;
+ public long gramCentCount;
/**
- * True, if the gift can be bought only using Toncoins.
+ * True, if the gift can be bought only using Grams.
*/
- public boolean toncoinOnly;
+ public boolean gramOnly;
/**
* Describes parameters of a unique gift available for resale.
@@ -27485,19 +27849,19 @@ public GiftResaleParameters() {
* Describes parameters of a unique gift available for resale.
*
* @param starCount Resale price of the gift in Telegram Stars.
- * @param toncoinCentCount Resale price of the gift in 1/100 of Toncoin.
- * @param toncoinOnly True, if the gift can be bought only using Toncoins.
+ * @param gramCentCount Resale price of the gift in 1/100 of TON Gram.
+ * @param gramOnly True, if the gift can be bought only using Grams.
*/
- public GiftResaleParameters(long starCount, long toncoinCentCount, boolean toncoinOnly) {
+ public GiftResaleParameters(long starCount, long gramCentCount, boolean gramOnly) {
this.starCount = starCount;
- this.toncoinCentCount = toncoinCentCount;
- this.toncoinOnly = toncoinOnly;
+ this.gramCentCount = gramCentCount;
+ this.gramOnly = gramOnly;
}
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = -2144380890;
+ public static final int CONSTRUCTOR = 1593292808;
/**
* @return this.CONSTRUCTOR
@@ -27519,7 +27883,7 @@ public abstract static class GiftResalePrice extends Object {
@Retention(RetentionPolicy.SOURCE)
@IntDef({
GiftResalePriceStar.CONSTRUCTOR,
- GiftResalePriceTon.CONSTRUCTOR
+ GiftResalePriceGram.CONSTRUCTOR
})
public @interface Constructors {}
@@ -27575,33 +27939,33 @@ public int getConstructor() {
}
/**
- * Describes price of a resold gift in Toncoins.
+ * Describes price of a resold gift in TON Grams.
*/
- public static class GiftResalePriceTon extends GiftResalePrice {
+ public static class GiftResalePriceGram extends GiftResalePrice {
/**
- * The amount of 1/100 of Toncoin expected to be paid for the gift. Must be in the range getOption("gift_resale_toncoin_cent_count_min")-getOption("gift_resale_toncoin_cent_count_max").
+ * The amount of 1/100 of Gram expected to be paid for the gift. Must be in the range getOption("gift_resale_gram_cent_count_min")-getOption("gift_resale_gram_cent_count_max").
*/
- public long toncoinCentCount;
+ public long gramCentCount;
/**
- * Describes price of a resold gift in Toncoins.
+ * Describes price of a resold gift in TON Grams.
*/
- public GiftResalePriceTon() {
+ public GiftResalePriceGram() {
}
/**
- * Describes price of a resold gift in Toncoins.
+ * Describes price of a resold gift in TON Grams.
*
- * @param toncoinCentCount The amount of 1/100 of Toncoin expected to be paid for the gift. Must be in the range getOption("gift_resale_toncoin_cent_count_min")-getOption("gift_resale_toncoin_cent_count_max").
+ * @param gramCentCount The amount of 1/100 of Gram expected to be paid for the gift. Must be in the range getOption("gift_resale_gram_cent_count_min")-getOption("gift_resale_gram_cent_count_max").
*/
- public GiftResalePriceTon(long toncoinCentCount) {
- this.toncoinCentCount = toncoinCentCount;
+ public GiftResalePriceGram(long gramCentCount) {
+ this.gramCentCount = gramCentCount;
}
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = -415435950;
+ public static final int CONSTRUCTOR = -1801800532;
/**
* @return this.CONSTRUCTOR
@@ -28430,7 +28794,7 @@ public int getConstructor() {
}
/**
- * The user can't participate in the giveaway, because they phone number is from a disallowed country.
+ * The user can't participate in the giveaway, because their phone number is from a disallowed country.
*/
public static class GiveawayParticipantStatusDisallowedCountry extends GiveawayParticipantStatus {
/**
@@ -28439,13 +28803,13 @@ public static class GiveawayParticipantStatusDisallowedCountry extends GiveawayP
public String userCountryCode;
/**
- * The user can't participate in the giveaway, because they phone number is from a disallowed country.
+ * The user can't participate in the giveaway, because their phone number is from a disallowed country.
*/
public GiveawayParticipantStatusDisallowedCountry() {
}
/**
- * The user can't participate in the giveaway, because they phone number is from a disallowed country.
+ * The user can't participate in the giveaway, because their phone number is from a disallowed country.
*
* @param userCountryCode A two-letter ISO 3166-1 alpha-2 country code of the user's country.
*/
@@ -28571,6 +28935,112 @@ public int getConstructor() {
}
}
+ /**
+ * A detailed statistics about TON Grams earned by the current user.
+ */
+ public static class GramRevenueStatistics extends Object {
+ /**
+ * A graph containing amount of revenue in a given day.
+ */
+ public StatisticalGraph revenueByDayGraph;
+ /**
+ * Amount of earned revenue.
+ */
+ public GramRevenueStatus status;
+ /**
+ * Current conversion rate of nanogram to USD cents.
+ */
+ public double usdRate;
+
+ /**
+ * A detailed statistics about TON Grams earned by the current user.
+ */
+ public GramRevenueStatistics() {
+ }
+
+ /**
+ * A detailed statistics about TON Grams earned by the current user.
+ *
+ * @param revenueByDayGraph A graph containing amount of revenue in a given day.
+ * @param status Amount of earned revenue.
+ * @param usdRate Current conversion rate of nanogram to USD cents.
+ */
+ public GramRevenueStatistics(StatisticalGraph revenueByDayGraph, GramRevenueStatus status, double usdRate) {
+ this.revenueByDayGraph = revenueByDayGraph;
+ this.status = status;
+ this.usdRate = usdRate;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = 44164778;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * Contains information about TON Grams earned by the current user.
+ */
+ public static class GramRevenueStatus extends Object {
+ /**
+ * Total Gram amount earned; in the smallest units of the cryptocurrency.
+ */
+ public long totalAmount;
+ /**
+ * The Gram amount that isn't withdrawn yet; in the smallest units of the cryptocurrency.
+ */
+ public long balanceAmount;
+ /**
+ * The Gram amount that is available for withdrawal; in the smallest units of the cryptocurrency.
+ */
+ public long availableAmount;
+ /**
+ * True, if Grams can be withdrawn.
+ */
+ public boolean withdrawalEnabled;
+
+ /**
+ * Contains information about TON Grams earned by the current user.
+ */
+ public GramRevenueStatus() {
+ }
+
+ /**
+ * Contains information about TON Grams earned by the current user.
+ *
+ * @param totalAmount Total Gram amount earned; in the smallest units of the cryptocurrency.
+ * @param balanceAmount The Gram amount that isn't withdrawn yet; in the smallest units of the cryptocurrency.
+ * @param availableAmount The Gram amount that is available for withdrawal; in the smallest units of the cryptocurrency.
+ * @param withdrawalEnabled True, if Grams can be withdrawn.
+ */
+ public GramRevenueStatus(long totalAmount, long balanceAmount, long availableAmount, boolean withdrawalEnabled) {
+ this.totalAmount = totalAmount;
+ this.balanceAmount = balanceAmount;
+ this.availableAmount = availableAmount;
+ this.withdrawalEnabled = withdrawalEnabled;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = -1681650727;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
/**
* Describes a group call.
*/
@@ -34323,19 +34793,7 @@ public static class InputMessageSticker extends InputMessageContent {
/**
* Sticker to be sent.
*/
- public InputFile sticker;
- /**
- * Sticker thumbnail; pass null to skip thumbnail uploading.
- */
- public InputThumbnail thumbnail;
- /**
- * Sticker width.
- */
- public int width;
- /**
- * Sticker height.
- */
- public int height;
+ public InputSticker sticker;
/**
* Emoji used to choose the sticker.
*/
@@ -34351,23 +34809,17 @@ public InputMessageSticker() {
* A sticker message.
*
* @param sticker Sticker to be sent.
- * @param thumbnail Sticker thumbnail; pass null to skip thumbnail uploading.
- * @param width Sticker width.
- * @param height Sticker height.
* @param emoji Emoji used to choose the sticker.
*/
- public InputMessageSticker(InputFile sticker, InputThumbnail thumbnail, int width, int height, String emoji) {
+ public InputMessageSticker(InputSticker sticker, String emoji) {
this.sticker = sticker;
- this.thumbnail = thumbnail;
- this.width = width;
- this.height = height;
this.emoji = emoji;
}
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = 1072805625;
+ public static final int CONSTRUCTOR = -1984702427;
/**
* @return this.CONSTRUCTOR
@@ -34445,21 +34897,9 @@ public int getConstructor() {
*/
public static class InputMessageVideoNote extends InputMessageContent {
/**
- * Video note to be sent. The video is expected to be encoded to MPEG4 format with H.264 codec and have no data outside of the visible circle.
- */
- public InputFile videoNote;
- /**
- * Video thumbnail; may be null if empty; pass null to skip thumbnail uploading.
- */
- @Nullable public InputThumbnail thumbnail;
- /**
- * Duration of the video, in seconds; 0-60.
- */
- public int duration;
- /**
- * Video width and height; must be positive and not greater than 640.
+ * Video note to be sent.
*/
- public int length;
+ public InputVideoNote videoNote;
/**
* Video note self-destruct type; may be null if none; pass null if none; private chats only.
*/
@@ -34474,24 +34914,18 @@ public InputMessageVideoNote() {
/**
* A video note message.
*
- * @param videoNote Video note to be sent. The video is expected to be encoded to MPEG4 format with H.264 codec and have no data outside of the visible circle.
- * @param thumbnail Video thumbnail; may be null if empty; pass null to skip thumbnail uploading.
- * @param duration Duration of the video, in seconds; 0-60.
- * @param length Video width and height; must be positive and not greater than 640.
+ * @param videoNote Video note to be sent.
* @param selfDestructType Video note self-destruct type; may be null if none; pass null if none; private chats only.
*/
- public InputMessageVideoNote(InputFile videoNote, InputThumbnail thumbnail, int duration, int length, MessageSelfDestructType selfDestructType) {
+ public InputMessageVideoNote(InputVideoNote videoNote, MessageSelfDestructType selfDestructType) {
this.videoNote = videoNote;
- this.thumbnail = thumbnail;
- this.duration = duration;
- this.length = length;
this.selfDestructType = selfDestructType;
}
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = -714598691;
+ public static final int CONSTRUCTOR = -1148919075;
/**
* @return this.CONSTRUCTOR
@@ -34507,17 +34941,9 @@ public int getConstructor() {
*/
public static class InputMessageVoiceNote extends InputMessageContent {
/**
- * Voice note to be sent. The voice note must be encoded with the Opus codec and stored inside an OGG container with a single audio channel, or be in MP3 or M4A format as regular audio.
- */
- public InputFile voiceNote;
- /**
- * Duration of the voice note, in seconds.
- */
- public int duration;
- /**
- * Waveform representation of the voice note in 5-bit format.
+ * Voice note to be sent.
*/
- public byte[] waveform;
+ public InputVoiceNote voiceNote;
/**
* Voice note caption; pass null to use an empty caption; 0-getOption("message_caption_length_max") characters.
*/
@@ -34536,16 +34962,12 @@ public InputMessageVoiceNote() {
/**
* A voice note message.
*
- * @param voiceNote Voice note to be sent. The voice note must be encoded with the Opus codec and stored inside an OGG container with a single audio channel, or be in MP3 or M4A format as regular audio.
- * @param duration Duration of the voice note, in seconds.
- * @param waveform Waveform representation of the voice note in 5-bit format.
+ * @param voiceNote Voice note to be sent.
* @param caption Voice note caption; pass null to use an empty caption; 0-getOption("message_caption_length_max") characters.
* @param selfDestructType Voice note self-destruct type; may be null if none; pass null if none; private chats only.
*/
- public InputMessageVoiceNote(InputFile voiceNote, int duration, byte[] waveform, FormattedText caption, MessageSelfDestructType selfDestructType) {
+ public InputMessageVoiceNote(InputVoiceNote voiceNote, FormattedText caption, MessageSelfDestructType selfDestructType) {
this.voiceNote = voiceNote;
- this.duration = duration;
- this.waveform = waveform;
this.caption = caption;
this.selfDestructType = selfDestructType;
}
@@ -34553,7 +34975,7 @@ public InputMessageVoiceNote(InputFile voiceNote, int duration, byte[] waveform,
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = 1461977004;
+ public static final int CONSTRUCTOR = -1141435043;
/**
* @return this.CONSTRUCTOR
@@ -35045,9 +35467,9 @@ public static class InputMessageStakeDice extends InputMessageContent {
*/
public String stateHash;
/**
- * The Toncoin amount that will be staked; in the smallest units of the currency. Must be in the range getOption("stake_dice_stake_amount_min")-getOption("stake_dice_stake_amount_max").
+ * The TON Gram amount that will be staked; in the smallest units of the currency. Must be in the range getOption("stake_dice_stake_amount_min")-getOption("stake_dice_stake_amount_max").
*/
- public long stakeToncoinAmount;
+ public long stakeGramAmount;
/**
* Pass true to delete message draft in the chat.
*/
@@ -35063,19 +35485,19 @@ public InputMessageStakeDice() {
* A stake dice message.
*
* @param stateHash Hash of the stake dice state. The state hash can be used only if it was received recently enough. Otherwise, a new state must be requested using getStakeDiceState.
- * @param stakeToncoinAmount The Toncoin amount that will be staked; in the smallest units of the currency. Must be in the range getOption("stake_dice_stake_amount_min")-getOption("stake_dice_stake_amount_max").
+ * @param stakeGramAmount The TON Gram amount that will be staked; in the smallest units of the currency. Must be in the range getOption("stake_dice_stake_amount_min")-getOption("stake_dice_stake_amount_max").
* @param clearDraft Pass true to delete message draft in the chat.
*/
- public InputMessageStakeDice(String stateHash, long stakeToncoinAmount, boolean clearDraft) {
+ public InputMessageStakeDice(String stateHash, long stakeGramAmount, boolean clearDraft) {
this.stateHash = stateHash;
- this.stakeToncoinAmount = stakeToncoinAmount;
+ this.stakeGramAmount = stakeGramAmount;
this.clearDraft = clearDraft;
}
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = 1946603673;
+ public static final int CONSTRUCTOR = 223775694;
/**
* @return this.CONSTRUCTOR
@@ -35248,7 +35670,8 @@ public abstract static class InputMessageReplyTo extends Object {
@IntDef({
InputMessageReplyToMessage.CONSTRUCTOR,
InputMessageReplyToExternalMessage.CONSTRUCTOR,
- InputMessageReplyToStory.CONSTRUCTOR
+ InputMessageReplyToStory.CONSTRUCTOR,
+ InputMessageReplyToEphemeralMessage.CONSTRUCTOR
})
public @interface Constructors {}
@@ -35427,6 +35850,1076 @@ public int getConstructor() {
}
}
+ /**
+ * Describes an ephemeral message to be replied; for bots only.
+ */
+ public static class InputMessageReplyToEphemeralMessage extends InputMessageReplyTo {
+ /**
+ * The identifier of the ephemeral message to be replied.
+ */
+ public int ephemeralMessageId;
+
+ /**
+ * Describes an ephemeral message to be replied; for bots only.
+ */
+ public InputMessageReplyToEphemeralMessage() {
+ }
+
+ /**
+ * Describes an ephemeral message to be replied; for bots only.
+ *
+ * @param ephemeralMessageId The identifier of the ephemeral message to be replied.
+ */
+ public InputMessageReplyToEphemeralMessage(int ephemeralMessageId) {
+ this.ephemeralMessageId = ephemeralMessageId;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = -1865090403;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * This class is an abstract base class.
+ * Describes a block of a rich message to send.
+ */
+ public abstract static class InputPageBlock extends Object {
+ /**
+ * Describes possible values returned by getConstructor().
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ InputPageBlockSectionHeading.CONSTRUCTOR,
+ InputPageBlockParagraph.CONSTRUCTOR,
+ InputPageBlockPreformatted.CONSTRUCTOR,
+ InputPageBlockFooter.CONSTRUCTOR,
+ InputPageBlockThinking.CONSTRUCTOR,
+ InputPageBlockDivider.CONSTRUCTOR,
+ InputPageBlockMathematicalExpression.CONSTRUCTOR,
+ InputPageBlockAnchor.CONSTRUCTOR,
+ InputPageBlockList.CONSTRUCTOR,
+ InputPageBlockBlockQuote.CONSTRUCTOR,
+ InputPageBlockPullQuote.CONSTRUCTOR,
+ InputPageBlockAnimation.CONSTRUCTOR,
+ InputPageBlockAudio.CONSTRUCTOR,
+ InputPageBlockPhoto.CONSTRUCTOR,
+ InputPageBlockVideo.CONSTRUCTOR,
+ InputPageBlockVoiceNote.CONSTRUCTOR,
+ InputPageBlockCollage.CONSTRUCTOR,
+ InputPageBlockSlideshow.CONSTRUCTOR,
+ InputPageBlockTable.CONSTRUCTOR,
+ InputPageBlockDetails.CONSTRUCTOR,
+ InputPageBlockMap.CONSTRUCTOR
+ })
+ public @interface Constructors {}
+
+ /**
+ * @return identifier uniquely determining type of the object.
+ */
+ @Constructors
+ @Override
+ public abstract int getConstructor();
+ /**
+ * Default class constructor.
+ */
+ public InputPageBlock() {
+ }
+ }
+
+ /**
+ * A section heading.
+ */
+ public static class InputPageBlockSectionHeading extends InputPageBlock {
+ /**
+ * Text of the section heading.
+ */
+ public RichText text;
+ /**
+ * Relative size of the text font; 1-6, 1 is the largest, 6 is the smallest.
+ */
+ public int size;
+
+ /**
+ * A section heading.
+ */
+ public InputPageBlockSectionHeading() {
+ }
+
+ /**
+ * A section heading.
+ *
+ * @param text Text of the section heading.
+ * @param size Relative size of the text font; 1-6, 1 is the largest, 6 is the smallest.
+ */
+ public InputPageBlockSectionHeading(RichText text, int size) {
+ this.text = text;
+ this.size = size;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = -990527072;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * A text paragraph.
+ */
+ public static class InputPageBlockParagraph extends InputPageBlock {
+ /**
+ * Paragraph text.
+ */
+ public RichText text;
+
+ /**
+ * A text paragraph.
+ */
+ public InputPageBlockParagraph() {
+ }
+
+ /**
+ * A text paragraph.
+ *
+ * @param text Paragraph text.
+ */
+ public InputPageBlockParagraph(RichText text) {
+ this.text = text;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = -766895937;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * A preformatted text paragraph.
+ */
+ public static class InputPageBlockPreformatted extends InputPageBlock {
+ /**
+ * Paragraph text.
+ */
+ public RichText text;
+ /**
+ * Programming language for which the text needs to be formatted.
+ */
+ public String language;
+
+ /**
+ * A preformatted text paragraph.
+ */
+ public InputPageBlockPreformatted() {
+ }
+
+ /**
+ * A preformatted text paragraph.
+ *
+ * @param text Paragraph text.
+ * @param language Programming language for which the text needs to be formatted.
+ */
+ public InputPageBlockPreformatted(RichText text, String language) {
+ this.text = text;
+ this.language = language;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = -1640868278;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * The footer of the page.
+ */
+ public static class InputPageBlockFooter extends InputPageBlock {
+ /**
+ * Footer.
+ */
+ public RichText footer;
+
+ /**
+ * The footer of the page.
+ */
+ public InputPageBlockFooter() {
+ }
+
+ /**
+ * The footer of the page.
+ *
+ * @param footer Footer.
+ */
+ public InputPageBlockFooter(RichText footer) {
+ this.footer = footer;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = 321372553;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * A "Thinking..." placeholder; for pending rich messages only; for bots only.
+ */
+ public static class InputPageBlockThinking extends InputPageBlock {
+ /**
+ * Text of the placeholder.
+ */
+ public RichText text;
+
+ /**
+ * A "Thinking..." placeholder; for pending rich messages only; for bots only.
+ */
+ public InputPageBlockThinking() {
+ }
+
+ /**
+ * A "Thinking..." placeholder; for pending rich messages only; for bots only.
+ *
+ * @param text Text of the placeholder.
+ */
+ public InputPageBlockThinking(RichText text) {
+ this.text = text;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = 551028356;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * An empty block separating the page.
+ */
+ public static class InputPageBlockDivider extends InputPageBlock {
+
+ /**
+ * An empty block separating the page.
+ */
+ public InputPageBlockDivider() {
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = -447652807;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * A mathematical expression.
+ */
+ public static class InputPageBlockMathematicalExpression extends InputPageBlock {
+ /**
+ * The expression in LaTeX format.
+ */
+ public String expression;
+
+ /**
+ * A mathematical expression.
+ */
+ public InputPageBlockMathematicalExpression() {
+ }
+
+ /**
+ * A mathematical expression.
+ *
+ * @param expression The expression in LaTeX format.
+ */
+ public InputPageBlockMathematicalExpression(String expression) {
+ this.expression = expression;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = 1177329767;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * An invisible anchor.
+ */
+ public static class InputPageBlockAnchor extends InputPageBlock {
+ /**
+ * Name of the anchor.
+ */
+ public String name;
+
+ /**
+ * An invisible anchor.
+ */
+ public InputPageBlockAnchor() {
+ }
+
+ /**
+ * An invisible anchor.
+ *
+ * @param name Name of the anchor.
+ */
+ public InputPageBlockAnchor(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = -870305959;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * A list of data blocks.
+ */
+ public static class InputPageBlockList extends InputPageBlock {
+ /**
+ * The items of the list.
+ */
+ public InputPageBlockListItem[] items;
+
+ /**
+ * A list of data blocks.
+ */
+ public InputPageBlockList() {
+ }
+
+ /**
+ * A list of data blocks.
+ *
+ * @param items The items of the list.
+ */
+ public InputPageBlockList(InputPageBlockListItem[] items) {
+ this.items = items;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = -84608677;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * A block quote.
+ */
+ public static class InputPageBlockBlockQuote extends InputPageBlock {
+ /**
+ * Quote blocks.
+ */
+ public InputPageBlock[] blocks;
+ /**
+ * Quote credit; pass null if none.
+ */
+ public RichText credit;
+
+ /**
+ * A block quote.
+ */
+ public InputPageBlockBlockQuote() {
+ }
+
+ /**
+ * A block quote.
+ *
+ * @param blocks Quote blocks.
+ * @param credit Quote credit; pass null if none.
+ */
+ public InputPageBlockBlockQuote(InputPageBlock[] blocks, RichText credit) {
+ this.blocks = blocks;
+ this.credit = credit;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = -440940450;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * A pull quote.
+ */
+ public static class InputPageBlockPullQuote extends InputPageBlock {
+ /**
+ * Quote text.
+ */
+ public RichText text;
+ /**
+ * Quote credit; pass null if none.
+ */
+ public RichText credit;
+
+ /**
+ * A pull quote.
+ */
+ public InputPageBlockPullQuote() {
+ }
+
+ /**
+ * A pull quote.
+ *
+ * @param text Quote text.
+ * @param credit Quote credit; pass null if none.
+ */
+ public InputPageBlockPullQuote(RichText text, RichText credit) {
+ this.text = text;
+ this.credit = credit;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = 86080004;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * An animation.
+ */
+ public static class InputPageBlockAnimation extends InputPageBlock {
+ /**
+ * The animation to be sent.
+ */
+ public InputAnimation animation;
+ /**
+ * Animation caption; pass null if none.
+ */
+ public PageBlockCaption caption;
+ /**
+ * True, if the animation preview must be covered by a spoiler animation.
+ */
+ public boolean hasSpoiler;
+
+ /**
+ * An animation.
+ */
+ public InputPageBlockAnimation() {
+ }
+
+ /**
+ * An animation.
+ *
+ * @param animation The animation to be sent.
+ * @param caption Animation caption; pass null if none.
+ * @param hasSpoiler True, if the animation preview must be covered by a spoiler animation.
+ */
+ public InputPageBlockAnimation(InputAnimation animation, PageBlockCaption caption, boolean hasSpoiler) {
+ this.animation = animation;
+ this.caption = caption;
+ this.hasSpoiler = hasSpoiler;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = 1405041485;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * An audio file.
+ */
+ public static class InputPageBlockAudio extends InputPageBlock {
+ /**
+ * The audio to be sent.
+ */
+ public InputAudio audio;
+ /**
+ * Audio file caption; pass null if none.
+ */
+ public PageBlockCaption caption;
+
+ /**
+ * An audio file.
+ */
+ public InputPageBlockAudio() {
+ }
+
+ /**
+ * An audio file.
+ *
+ * @param audio The audio to be sent.
+ * @param caption Audio file caption; pass null if none.
+ */
+ public InputPageBlockAudio(InputAudio audio, PageBlockCaption caption) {
+ this.audio = audio;
+ this.caption = caption;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = -652853196;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * A photo.
+ */
+ public static class InputPageBlockPhoto extends InputPageBlock {
+ /**
+ * The photo to be sent.
+ */
+ public InputPhoto photo;
+ /**
+ * Photo caption; pass null if none.
+ */
+ public PageBlockCaption caption;
+ /**
+ * True, if the photo preview must be covered by a spoiler animation.
+ */
+ public boolean hasSpoiler;
+
+ /**
+ * A photo.
+ */
+ public InputPageBlockPhoto() {
+ }
+
+ /**
+ * A photo.
+ *
+ * @param photo The photo to be sent.
+ * @param caption Photo caption; pass null if none.
+ * @param hasSpoiler True, if the photo preview must be covered by a spoiler animation.
+ */
+ public InputPageBlockPhoto(InputPhoto photo, PageBlockCaption caption, boolean hasSpoiler) {
+ this.photo = photo;
+ this.caption = caption;
+ this.hasSpoiler = hasSpoiler;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = -322327456;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * A video.
+ */
+ public static class InputPageBlockVideo extends InputPageBlock {
+ /**
+ * The video to be sent.
+ */
+ public InputVideo video;
+ /**
+ * Video caption; pass null if none.
+ */
+ public PageBlockCaption caption;
+ /**
+ * True, if the video preview must be covered by a spoiler animation.
+ */
+ public boolean hasSpoiler;
+
+ /**
+ * A video.
+ */
+ public InputPageBlockVideo() {
+ }
+
+ /**
+ * A video.
+ *
+ * @param video The video to be sent.
+ * @param caption Video caption; pass null if none.
+ * @param hasSpoiler True, if the video preview must be covered by a spoiler animation.
+ */
+ public InputPageBlockVideo(InputVideo video, PageBlockCaption caption, boolean hasSpoiler) {
+ this.video = video;
+ this.caption = caption;
+ this.hasSpoiler = hasSpoiler;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = -364001716;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * A voice note.
+ */
+ public static class InputPageBlockVoiceNote extends InputPageBlock {
+ /**
+ * The voice note to be sent.
+ */
+ public InputVoiceNote voiceNote;
+ /**
+ * Voice note caption; pass null if none.
+ */
+ public PageBlockCaption caption;
+
+ /**
+ * A voice note.
+ */
+ public InputPageBlockVoiceNote() {
+ }
+
+ /**
+ * A voice note.
+ *
+ * @param voiceNote The voice note to be sent.
+ * @param caption Voice note caption; pass null if none.
+ */
+ public InputPageBlockVoiceNote(InputVoiceNote voiceNote, PageBlockCaption caption) {
+ this.voiceNote = voiceNote;
+ this.caption = caption;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = -1336626255;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * A collage.
+ */
+ public static class InputPageBlockCollage extends InputPageBlock {
+ /**
+ * Collage item contents.
+ */
+ public InputPageBlock[] blocks;
+ /**
+ * Block caption; pass null if none.
+ */
+ public PageBlockCaption caption;
+
+ /**
+ * A collage.
+ */
+ public InputPageBlockCollage() {
+ }
+
+ /**
+ * A collage.
+ *
+ * @param blocks Collage item contents.
+ * @param caption Block caption; pass null if none.
+ */
+ public InputPageBlockCollage(InputPageBlock[] blocks, PageBlockCaption caption) {
+ this.blocks = blocks;
+ this.caption = caption;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = -1311353404;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * A slideshow.
+ */
+ public static class InputPageBlockSlideshow extends InputPageBlock {
+ /**
+ * Slideshow item contents.
+ */
+ public InputPageBlock[] blocks;
+ /**
+ * Block caption; pass null if none.
+ */
+ public PageBlockCaption caption;
+
+ /**
+ * A slideshow.
+ */
+ public InputPageBlockSlideshow() {
+ }
+
+ /**
+ * A slideshow.
+ *
+ * @param blocks Slideshow item contents.
+ * @param caption Block caption; pass null if none.
+ */
+ public InputPageBlockSlideshow(InputPageBlock[] blocks, PageBlockCaption caption) {
+ this.blocks = blocks;
+ this.caption = caption;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = -979922320;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * A table.
+ */
+ public static class InputPageBlockTable extends InputPageBlock {
+ /**
+ * Table caption.
+ */
+ public RichText caption;
+ /**
+ * Table cells.
+ */
+ public PageBlockTableCell[][] cells;
+ /**
+ * True, if the table is bordered.
+ */
+ public boolean isBordered;
+ /**
+ * True, if the table is striped.
+ */
+ public boolean isStriped;
+
+ /**
+ * A table.
+ */
+ public InputPageBlockTable() {
+ }
+
+ /**
+ * A table.
+ *
+ * @param caption Table caption.
+ * @param cells Table cells.
+ * @param isBordered True, if the table is bordered.
+ * @param isStriped True, if the table is striped.
+ */
+ public InputPageBlockTable(RichText caption, PageBlockTableCell[][] cells, boolean isBordered, boolean isStriped) {
+ this.caption = caption;
+ this.cells = cells;
+ this.isBordered = isBordered;
+ this.isStriped = isStriped;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = 817721599;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * A collapsible block.
+ */
+ public static class InputPageBlockDetails extends InputPageBlock {
+ /**
+ * Always visible heading for the block.
+ */
+ public RichText header;
+ /**
+ * Block contents.
+ */
+ public InputPageBlock[] blocks;
+ /**
+ * True, if the block is open by default.
+ */
+ public boolean isOpen;
+
+ /**
+ * A collapsible block.
+ */
+ public InputPageBlockDetails() {
+ }
+
+ /**
+ * A collapsible block.
+ *
+ * @param header Always visible heading for the block.
+ * @param blocks Block contents.
+ * @param isOpen True, if the block is open by default.
+ */
+ public InputPageBlockDetails(RichText header, InputPageBlock[] blocks, boolean isOpen) {
+ this.header = header;
+ this.blocks = blocks;
+ this.isOpen = isOpen;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = -517140887;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * A map. The map's width and height must not exceed 10000 in total. Width and height ratio must be at most 20.
+ */
+ public static class InputPageBlockMap extends InputPageBlock {
+ /**
+ * Location of the map center.
+ */
+ public Location location;
+ /**
+ * Map zoom level; 0-24.
+ */
+ public int zoom;
+ /**
+ * Map width; 0-10000.
+ */
+ public int width;
+ /**
+ * Map height; 0-10000.
+ */
+ public int height;
+ /**
+ * Block caption; pass null if none.
+ */
+ public PageBlockCaption caption;
+
+ /**
+ * A map. The map's width and height must not exceed 10000 in total. Width and height ratio must be at most 20.
+ */
+ public InputPageBlockMap() {
+ }
+
+ /**
+ * A map. The map's width and height must not exceed 10000 in total. Width and height ratio must be at most 20.
+ *
+ * @param location Location of the map center.
+ * @param zoom Map zoom level; 0-24.
+ * @param width Map width; 0-10000.
+ * @param height Map height; 0-10000.
+ * @param caption Block caption; pass null if none.
+ */
+ public InputPageBlockMap(Location location, int zoom, int width, int height, PageBlockCaption caption) {
+ this.location = location;
+ this.zoom = zoom;
+ this.width = width;
+ this.height = height;
+ this.caption = caption;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = 2051912833;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * Describes an item of a list page block to be sent.
+ */
+ public static class InputPageBlockListItem extends Object {
+ /**
+ * Item blocks.
+ */
+ public InputPageBlock[] blocks;
+ /**
+ * True, if the item has a checkbox.
+ */
+ public boolean hasCheckbox;
+ /**
+ * True, if the item is checked.
+ */
+ public boolean isChecked;
+ /**
+ * Value of the item; pass 0 for unordered lists.
+ */
+ public int value;
+ /**
+ * Type of the item numbering type; must be one of "a" for a lowercase letter, "A" for an uppercase letter, "i" for lowercase Roman numerals, "I" for uppercase Roman numerals, "1" for decimal numbers, or empty for unordered lists.
+ */
+ public String type;
+
+ /**
+ * Describes an item of a list page block to be sent.
+ */
+ public InputPageBlockListItem() {
+ }
+
+ /**
+ * Describes an item of a list page block to be sent.
+ *
+ * @param blocks Item blocks.
+ * @param hasCheckbox True, if the item has a checkbox.
+ * @param isChecked True, if the item is checked.
+ * @param value Value of the item; pass 0 for unordered lists.
+ * @param type Type of the item numbering type; must be one of "a" for a lowercase letter, "A" for an uppercase letter, "i" for lowercase Roman numerals, "I" for uppercase Roman numerals, "1" for decimal numbers, or empty for unordered lists.
+ */
+ public InputPageBlockListItem(InputPageBlock[] blocks, boolean hasCheckbox, boolean isChecked, int value, String type) {
+ this.blocks = blocks;
+ this.hasCheckbox = hasCheckbox;
+ this.isChecked = isChecked;
+ this.value = value;
+ this.type = type;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = -994904638;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
/**
* Describes a paid media to be sent.
*/
@@ -36965,19 +38458,7 @@ public static class InputPollMediaSticker extends InputPollMedia {
/**
* Sticker to be sent.
*/
- public InputFile sticker;
- /**
- * Sticker thumbnail; pass null to skip thumbnail uploading.
- */
- public InputThumbnail thumbnail;
- /**
- * Sticker width.
- */
- public int width;
- /**
- * Sticker height.
- */
- public int height;
+ public InputSticker sticker;
/**
* A sticker.
@@ -36989,21 +38470,15 @@ public InputPollMediaSticker() {
* A sticker.
*
* @param sticker Sticker to be sent.
- * @param thumbnail Sticker thumbnail; pass null to skip thumbnail uploading.
- * @param width Sticker width.
- * @param height Sticker height.
*/
- public InputPollMediaSticker(InputFile sticker, InputThumbnail thumbnail, int width, int height) {
+ public InputPollMediaSticker(InputSticker sticker) {
this.sticker = sticker;
- this.thumbnail = thumbnail;
- this.width = width;
- this.height = height;
}
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = -1948870442;
+ public static final int CONSTRUCTOR = -1605019153;
/**
* @return this.CONSTRUCTOR
@@ -37251,7 +38726,7 @@ public int getConstructor() {
}
/**
- * A rich message to send.
+ * A rich message to send. Total length of all texts, including custom emoji alternative text and formula source, must not exceed getOption("rich_message_text_length_max"). The total number of all blocks, list items and table rows must not exceed getOption("rich_message_block_count_max"). The maximum allowed depth of nested blocks and rich texts is getOption("rich_message_depth_max"). The total number of media in all blocks must not exceed getOption("rich_message_media_count_max"). The maximum allowed number of table columns is getOption("rich_message_table_column_count_max").
*/
public static class InputRichMessage extends Object {
/**
@@ -37268,13 +38743,13 @@ public static class InputRichMessage extends Object {
public boolean detectAutomaticBlocks;
/**
- * A rich message to send.
+ * A rich message to send. Total length of all texts, including custom emoji alternative text and formula source, must not exceed getOption("rich_message_text_length_max"). The total number of all blocks, list items and table rows must not exceed getOption("rich_message_block_count_max"). The maximum allowed depth of nested blocks and rich texts is getOption("rich_message_depth_max"). The total number of media in all blocks must not exceed getOption("rich_message_media_count_max"). The maximum allowed number of table columns is getOption("rich_message_table_column_count_max").
*/
public InputRichMessage() {
}
/**
- * A rich message to send.
+ * A rich message to send. Total length of all texts, including custom emoji alternative text and formula source, must not exceed getOption("rich_message_text_length_max"). The total number of all blocks, list items and table rows must not exceed getOption("rich_message_block_count_max"). The maximum allowed depth of nested blocks and rich texts is getOption("rich_message_depth_max"). The total number of media in all blocks must not exceed getOption("rich_message_media_count_max"). The maximum allowed number of table columns is getOption("rich_message_table_column_count_max").
*
* @param source Source of the rich message.
* @param isRtl Pass true if the message must be shown from right to left.
@@ -37301,57 +38776,95 @@ public int getConstructor() {
}
/**
- * A sticker to be added to a sticker set.
+ * Describes a media to be used in a sent rich message.
*/
- public static class InputSticker extends Object {
+ public static class InputRichMessageMedia extends Object {
/**
- * File with the sticker; must fit in a 512x512 square. For WEBP stickers the file must be in WEBP or PNG format, which will be converted to WEBP server-side. See https://core.telegram.org/animated_stickers#technical-requirements for technical requirements.
+ * Unique identifier of the media; 1-64 base64url characters.
*/
- public InputFile sticker;
+ public String id;
/**
- * Format of the sticker.
+ * The media to send. Must be one of the following types: inputMessageAnimation, inputMessageAudio, inputMessagePhoto, inputMessageVideo, or inputMessageVoiceNote.
*/
- public StickerFormat format;
+ public InputMessageContent media;
+
/**
- * String with 1-20 emoji corresponding to the sticker.
+ * Describes a media to be used in a sent rich message.
*/
- public String emojis;
+ public InputRichMessageMedia() {
+ }
+
/**
- * Position where the mask is placed; pass null if not specified.
+ * Describes a media to be used in a sent rich message.
+ *
+ * @param id Unique identifier of the media; 1-64 base64url characters.
+ * @param media The media to send. Must be one of the following types: inputMessageAnimation, inputMessageAudio, inputMessagePhoto, inputMessageVideo, or inputMessageVoiceNote.
*/
- public MaskPosition maskPosition;
+ public InputRichMessageMedia(String id, InputMessageContent media) {
+ this.id = id;
+ this.media = media;
+ }
+
/**
- * List of up to 20 keywords with total length up to 64 characters, which can be used to find the sticker.
+ * Identifier uniquely determining type of the object.
*/
- public String[] keywords;
+ public static final int CONSTRUCTOR = -1449915987;
/**
- * A sticker to be added to a sticker set.
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * A sticker to be sent.
+ */
+ public static class InputSticker extends Object {
+ /**
+ * Sticker to be sent.
+ */
+ public InputFile sticker;
+ /**
+ * Sticker thumbnail; pass null to skip thumbnail uploading.
+ */
+ public InputThumbnail thumbnail;
+ /**
+ * Sticker width.
+ */
+ public int width;
+ /**
+ * Sticker height.
+ */
+ public int height;
+
+ /**
+ * A sticker to be sent.
*/
public InputSticker() {
}
/**
- * A sticker to be added to a sticker set.
+ * A sticker to be sent.
*
- * @param sticker File with the sticker; must fit in a 512x512 square. For WEBP stickers the file must be in WEBP or PNG format, which will be converted to WEBP server-side. See https://core.telegram.org/animated_stickers#technical-requirements for technical requirements.
- * @param format Format of the sticker.
- * @param emojis String with 1-20 emoji corresponding to the sticker.
- * @param maskPosition Position where the mask is placed; pass null if not specified.
- * @param keywords List of up to 20 keywords with total length up to 64 characters, which can be used to find the sticker.
+ * @param sticker Sticker to be sent.
+ * @param thumbnail Sticker thumbnail; pass null to skip thumbnail uploading.
+ * @param width Sticker width.
+ * @param height Sticker height.
*/
- public InputSticker(InputFile sticker, StickerFormat format, String emojis, MaskPosition maskPosition, String[] keywords) {
+ public InputSticker(InputFile sticker, InputThumbnail thumbnail, int width, int height) {
this.sticker = sticker;
- this.format = format;
- this.emojis = emojis;
- this.maskPosition = maskPosition;
- this.keywords = keywords;
+ this.thumbnail = thumbnail;
+ this.width = width;
+ this.height = height;
}
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = 1589392402;
+ public static final int CONSTRUCTOR = -956316144;
/**
* @return this.CONSTRUCTOR
@@ -38188,6 +39701,112 @@ public int getConstructor() {
}
}
+ /**
+ * A video note to be sent.
+ */
+ public static class InputVideoNote extends Object {
+ /**
+ * Video note file to be sent. The video is expected to be encoded to MPEG4 format with H.264 codec and have no data outside of the visible circle.
+ */
+ public InputFile videoNote;
+ /**
+ * Video thumbnail; may be null if empty; pass null to skip thumbnail uploading.
+ */
+ @Nullable public InputThumbnail thumbnail;
+ /**
+ * Duration of the video, in seconds; 0-60.
+ */
+ public int duration;
+ /**
+ * Video width and height; must be positive and not greater than 640.
+ */
+ public int length;
+
+ /**
+ * A video note to be sent.
+ */
+ public InputVideoNote() {
+ }
+
+ /**
+ * A video note to be sent.
+ *
+ * @param videoNote Video note file to be sent. The video is expected to be encoded to MPEG4 format with H.264 codec and have no data outside of the visible circle.
+ * @param thumbnail Video thumbnail; may be null if empty; pass null to skip thumbnail uploading.
+ * @param duration Duration of the video, in seconds; 0-60.
+ * @param length Video width and height; must be positive and not greater than 640.
+ */
+ public InputVideoNote(InputFile videoNote, InputThumbnail thumbnail, int duration, int length) {
+ this.videoNote = videoNote;
+ this.thumbnail = thumbnail;
+ this.duration = duration;
+ this.length = length;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = 462853190;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * A video note to be sent.
+ */
+ public static class InputVoiceNote extends Object {
+ /**
+ * Voice note file to be sent. The voice note must be encoded with the Opus codec and stored inside an OGG container with a single audio channel, or be in MP3 or M4A format as regular audio.
+ */
+ public InputFile voiceNote;
+ /**
+ * Duration of the voice note, in seconds.
+ */
+ public int duration;
+ /**
+ * Waveform representation of the voice note in 5-bit format.
+ */
+ public byte[] waveform;
+
+ /**
+ * A video note to be sent.
+ */
+ public InputVoiceNote() {
+ }
+
+ /**
+ * A video note to be sent.
+ *
+ * @param voiceNote Voice note file to be sent. The voice note must be encoded with the Opus codec and stored inside an OGG container with a single audio channel, or be in MP3 or M4A format as regular audio.
+ * @param duration Duration of the voice note, in seconds.
+ * @param waveform Waveform representation of the voice note in 5-bit format.
+ */
+ public InputVoiceNote(InputFile voiceNote, int duration, byte[] waveform) {
+ this.voiceNote = voiceNote;
+ this.duration = duration;
+ this.waveform = waveform;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = -1965682819;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
/**
* This class is an abstract base class.
* Describes an internal https://t.me or tg: link, which must be processed by the application in a special way.
@@ -45088,6 +46707,10 @@ public static class Message extends Object {
* Identifier of the sender of the message.
*/
public MessageSender senderId;
+ /**
+ * Identifier of the user or the chat which received the ephemeral message; may be null. Always null for non-ephemeral messages.
+ */
+ @Nullable public MessageSender receiverId;
/**
* Chat identifier.
*/
@@ -45129,9 +46752,9 @@ public static class Message extends Object {
*/
public boolean isPaidStarSuggestedPost;
/**
- * True, if the message is a suggested channel post which was paid in Toncoins; a warning must be shown if the message is deleted in less than getOption("suggested_post_lifetime_min") seconds after sending.
+ * True, if the message is a suggested channel post which was paid in TON Grams; a warning must be shown if the message is deleted in less than getOption("suggested_post_lifetime_min") seconds after sending.
*/
- public boolean isPaidTonSuggestedPost;
+ public boolean isPaidGramSuggestedPost;
/**
* True, if the message contains an unread mention for the current user.
*/
@@ -45145,7 +46768,7 @@ public static class Message extends Object {
*/
public int date;
/**
- * Point in time (Unix timestamp) when the message was last edited; 0 for scheduled messages.
+ * Point in time (Unix timestamp) when the message was last edited; 0 for scheduled messages. If getOption("show_message_edit_date_by_default") is true, then the date must be shown along with the message instead of the date when the message was sent.
*/
public int editDate;
/**
@@ -45244,6 +46867,10 @@ public static class Message extends Object {
* Reply markup for the message; may be null if none.
*/
@Nullable public ReplyMarkup replyMarkup;
+ /**
+ * Unique identifier of the ephemeral message if the message is ephemeral; for bots only.
+ */
+ public int ephemeralMessageId;
/**
* Describes a message.
@@ -45256,6 +46883,7 @@ public Message() {
*
* @param id Message identifier; unique for the chat to which the message belongs.
* @param senderId Identifier of the sender of the message.
+ * @param receiverId Identifier of the user or the chat which received the ephemeral message; may be null. Always null for non-ephemeral messages.
* @param chatId Chat identifier.
* @param sendingState The sending state of the message; may be null if the message isn't being sent and didn't fail to be sent.
* @param schedulingState The scheduling state of the message; may be null if the message isn't scheduled.
@@ -45266,11 +46894,11 @@ public Message() {
* @param hasTimestampedMedia True, if media timestamp entities refers to a media in this message as opposed to a media in the replied message.
* @param isChannelPost True, if the message is a channel post. All messages to channels are channel posts, all other messages are not channel posts.
* @param isPaidStarSuggestedPost True, if the message is a suggested channel post which was paid in Telegram Stars; a warning must be shown if the message is deleted in less than getOption("suggested_post_lifetime_min") seconds after sending.
- * @param isPaidTonSuggestedPost True, if the message is a suggested channel post which was paid in Toncoins; a warning must be shown if the message is deleted in less than getOption("suggested_post_lifetime_min") seconds after sending.
+ * @param isPaidGramSuggestedPost True, if the message is a suggested channel post which was paid in TON Grams; a warning must be shown if the message is deleted in less than getOption("suggested_post_lifetime_min") seconds after sending.
* @param containsUnreadMention True, if the message contains an unread mention for the current user.
* @param containsUnreadPollVotes True, if the message is a poll message with unread votes.
* @param date Point in time (Unix timestamp) when the message was sent; 0 for scheduled messages.
- * @param editDate Point in time (Unix timestamp) when the message was last edited; 0 for scheduled messages.
+ * @param editDate Point in time (Unix timestamp) when the message was last edited; 0 for scheduled messages. If getOption("show_message_edit_date_by_default") is true, then the date must be shown along with the message instead of the date when the message was sent.
* @param forwardInfo Information about the initial message sender; may be null if none or unknown.
* @param importInfo Information about the initial message for messages created with importMessages; may be null if the message isn't imported.
* @param interactionInfo Information about interactions with the message; may be null if none.
@@ -45295,10 +46923,12 @@ public Message() {
* @param summaryLanguageCode IETF language tag of the message language on which it can be summarized; empty if summary isn't available for the message.
* @param content Content of the message.
* @param replyMarkup Reply markup for the message; may be null if none.
+ * @param ephemeralMessageId Unique identifier of the ephemeral message if the message is ephemeral; for bots only.
*/
- public Message(long id, MessageSender senderId, long chatId, MessageSendingState sendingState, MessageSchedulingState schedulingState, boolean isOutgoing, boolean isPinned, boolean isFromOffline, boolean canBeSaved, boolean hasTimestampedMedia, boolean isChannelPost, boolean isPaidStarSuggestedPost, boolean isPaidTonSuggestedPost, boolean containsUnreadMention, boolean containsUnreadPollVotes, int date, int editDate, MessageForwardInfo forwardInfo, MessageImportInfo importInfo, MessageInteractionInfo interactionInfo, UnreadReaction[] unreadReactions, FactCheck factCheck, SuggestedPostInfo suggestedPostInfo, MessageReplyTo replyTo, MessageTopic topicId, MessageSelfDestructType selfDestructType, double selfDestructIn, double autoDeleteIn, long viaBotUserId, MessageSender guestBotCallerId, long senderBusinessBotUserId, int senderBoostCount, String senderTag, long paidMessageStarCount, String authorSignature, long mediaAlbumId, long effectId, RestrictionInfo restrictionInfo, String summaryLanguageCode, MessageContent content, ReplyMarkup replyMarkup) {
+ public Message(long id, MessageSender senderId, MessageSender receiverId, long chatId, MessageSendingState sendingState, MessageSchedulingState schedulingState, boolean isOutgoing, boolean isPinned, boolean isFromOffline, boolean canBeSaved, boolean hasTimestampedMedia, boolean isChannelPost, boolean isPaidStarSuggestedPost, boolean isPaidGramSuggestedPost, boolean containsUnreadMention, boolean containsUnreadPollVotes, int date, int editDate, MessageForwardInfo forwardInfo, MessageImportInfo importInfo, MessageInteractionInfo interactionInfo, UnreadReaction[] unreadReactions, FactCheck factCheck, SuggestedPostInfo suggestedPostInfo, MessageReplyTo replyTo, MessageTopic topicId, MessageSelfDestructType selfDestructType, double selfDestructIn, double autoDeleteIn, long viaBotUserId, MessageSender guestBotCallerId, long senderBusinessBotUserId, int senderBoostCount, String senderTag, long paidMessageStarCount, String authorSignature, long mediaAlbumId, long effectId, RestrictionInfo restrictionInfo, String summaryLanguageCode, MessageContent content, ReplyMarkup replyMarkup, int ephemeralMessageId) {
this.id = id;
this.senderId = senderId;
+ this.receiverId = receiverId;
this.chatId = chatId;
this.sendingState = sendingState;
this.schedulingState = schedulingState;
@@ -45309,7 +46939,7 @@ public Message(long id, MessageSender senderId, long chatId, MessageSendingState
this.hasTimestampedMedia = hasTimestampedMedia;
this.isChannelPost = isChannelPost;
this.isPaidStarSuggestedPost = isPaidStarSuggestedPost;
- this.isPaidTonSuggestedPost = isPaidTonSuggestedPost;
+ this.isPaidGramSuggestedPost = isPaidGramSuggestedPost;
this.containsUnreadMention = containsUnreadMention;
this.containsUnreadPollVotes = containsUnreadPollVotes;
this.date = date;
@@ -45338,12 +46968,13 @@ public Message(long id, MessageSender senderId, long chatId, MessageSendingState
this.summaryLanguageCode = summaryLanguageCode;
this.content = content;
this.replyMarkup = replyMarkup;
+ this.ephemeralMessageId = ephemeralMessageId;
}
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = -609581767;
+ public static final int CONSTRUCTOR = -1553901480;
/**
* @return this.CONSTRUCTOR
@@ -45538,6 +47169,8 @@ public abstract static class MessageContent extends Object {
MessageChatJoinByLink.CONSTRUCTOR,
MessageChatJoinByRequest.CONSTRUCTOR,
MessageChatDeleteMember.CONSTRUCTOR,
+ MessageChatAddedToCommunity.CONSTRUCTOR,
+ MessageChatRemovedFromCommunity.CONSTRUCTOR,
MessageChatUpgradeTo.CONSTRUCTOR,
MessageChatUpgradeFrom.CONSTRUCTOR,
MessagePinMessage.CONSTRUCTOR,
@@ -46675,13 +48308,13 @@ public static class MessageStakeDice extends MessageContent {
*/
public int value;
/**
- * The Toncoin amount that was staked; in the smallest units of the currency.
+ * The TON Gram amount that was staked; in the smallest units of the currency.
*/
- public long stakeToncoinAmount;
+ public long stakeGramAmount;
/**
- * The Toncoin amount that was gained from the roll; in the smallest units of the currency; -1 if the dice don't have final state yet.
+ * The TON Gram amount that was gained from the roll; in the smallest units of the currency; -1 if the dice don't have final state yet.
*/
- public long prizeToncoinAmount;
+ public long prizeGramAmount;
/**
* A stake dice message. The dice value is randomly generated by the server.
@@ -46695,21 +48328,21 @@ public MessageStakeDice() {
* @param initialState The animated stickers with the initial dice animation; may be null if unknown. The update updateMessageContent will be sent when the sticker became known.
* @param finalState The animated stickers with the final dice animation; may be null if unknown. The update updateMessageContent will be sent when the sticker became known.
* @param value The dice value. If the value is 0, then the dice don't have final state yet.
- * @param stakeToncoinAmount The Toncoin amount that was staked; in the smallest units of the currency.
- * @param prizeToncoinAmount The Toncoin amount that was gained from the roll; in the smallest units of the currency; -1 if the dice don't have final state yet.
+ * @param stakeGramAmount The TON Gram amount that was staked; in the smallest units of the currency.
+ * @param prizeGramAmount The TON Gram amount that was gained from the roll; in the smallest units of the currency; -1 if the dice don't have final state yet.
*/
- public MessageStakeDice(DiceStickers initialState, DiceStickers finalState, int value, long stakeToncoinAmount, long prizeToncoinAmount) {
+ public MessageStakeDice(DiceStickers initialState, DiceStickers finalState, int value, long stakeGramAmount, long prizeGramAmount) {
this.initialState = initialState;
this.finalState = finalState;
this.value = value;
- this.stakeToncoinAmount = stakeToncoinAmount;
- this.prizeToncoinAmount = prizeToncoinAmount;
+ this.stakeGramAmount = stakeGramAmount;
+ this.prizeGramAmount = prizeGramAmount;
}
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = 844428448;
+ public static final int CONSTRUCTOR = 846324273;
/**
* @return this.CONSTRUCTOR
@@ -47755,6 +49388,69 @@ public int getConstructor() {
}
}
+ /**
+ * The chat was added to a community.
+ */
+ public static class MessageChatAddedToCommunity extends MessageContent {
+ /**
+ * Identifier of the community to which the chat was added.
+ */
+ public long communityId;
+
+ /**
+ * The chat was added to a community.
+ */
+ public MessageChatAddedToCommunity() {
+ }
+
+ /**
+ * The chat was added to a community.
+ *
+ * @param communityId Identifier of the community to which the chat was added.
+ */
+ public MessageChatAddedToCommunity(long communityId) {
+ this.communityId = communityId;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = -1000122284;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * The chat was removed from a community.
+ */
+ public static class MessageChatRemovedFromCommunity extends MessageContent {
+
+ /**
+ * The chat was removed from a community.
+ */
+ public MessageChatRemovedFromCommunity() {
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = 583449813;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
/**
* A basic group was upgraded to a supergroup and was deactivated as the result.
*/
@@ -49219,23 +50915,23 @@ public int getConstructor() {
}
/**
- * Toncoins were gifted to a user.
+ * TON Grams were gifted to a user.
*/
public static class MessageGiftedTon extends MessageContent {
/**
- * The identifier of a user who gifted Toncoins; 0 if the gift was anonymous or is outgoing.
+ * The identifier of a user who gifted Grams; 0 if the gift was anonymous or is outgoing.
*/
public long gifterUserId;
/**
- * The identifier of a user who received Toncoins; 0 if the gift is incoming.
+ * The identifier of a user who received Grams; 0 if the gift is incoming.
*/
public long receiverUserId;
/**
- * The received Toncoin amount, in the smallest units of the cryptocurrency.
+ * The received Gram amount, in the smallest units of the cryptocurrency.
*/
- public long tonAmount;
+ public long gramAmount;
/**
- * Identifier of the transaction for Toncoin credit; for receiver only.
+ * Identifier of the transaction for Gram credit; for receiver only.
*/
public String transactionId;
/**
@@ -49244,24 +50940,24 @@ public static class MessageGiftedTon extends MessageContent {
@Nullable public Sticker sticker;
/**
- * Toncoins were gifted to a user.
+ * TON Grams were gifted to a user.
*/
public MessageGiftedTon() {
}
/**
- * Toncoins were gifted to a user.
+ * TON Grams were gifted to a user.
*
- * @param gifterUserId The identifier of a user who gifted Toncoins; 0 if the gift was anonymous or is outgoing.
- * @param receiverUserId The identifier of a user who received Toncoins; 0 if the gift is incoming.
- * @param tonAmount The received Toncoin amount, in the smallest units of the cryptocurrency.
- * @param transactionId Identifier of the transaction for Toncoin credit; for receiver only.
+ * @param gifterUserId The identifier of a user who gifted Grams; 0 if the gift was anonymous or is outgoing.
+ * @param receiverUserId The identifier of a user who received Grams; 0 if the gift is incoming.
+ * @param gramAmount The received Gram amount, in the smallest units of the cryptocurrency.
+ * @param transactionId Identifier of the transaction for Gram credit; for receiver only.
* @param sticker A sticker to be shown in the message; may be null if unknown.
*/
- public MessageGiftedTon(long gifterUserId, long receiverUserId, long tonAmount, String transactionId, Sticker sticker) {
+ public MessageGiftedTon(long gifterUserId, long receiverUserId, long gramAmount, String transactionId, Sticker sticker) {
this.gifterUserId = gifterUserId;
this.receiverUserId = receiverUserId;
- this.tonAmount = tonAmount;
+ this.gramAmount = gramAmount;
this.transactionId = transactionId;
this.sticker = sticker;
}
@@ -49269,7 +50965,7 @@ public MessageGiftedTon(long gifterUserId, long receiverUserId, long tonAmount,
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = 766483995;
+ public static final int CONSTRUCTOR = 941235753;
/**
* @return this.CONSTRUCTOR
@@ -50149,9 +51845,9 @@ public static class MessageSuggestedPostPaid extends MessageContent {
*/
public StarAmount starAmount;
/**
- * The amount of received Toncoins; in the smallest units of the cryptocurrency.
+ * The amount of received TON Grams; in the smallest units of the cryptocurrency.
*/
- public long tonAmount;
+ public long gramAmount;
/**
* A suggested post was published for getOption("suggested_post_lifetime_min") seconds and payment for the post was received.
@@ -50164,18 +51860,18 @@ public MessageSuggestedPostPaid() {
*
* @param suggestedPostMessageId Identifier of the message with the suggested post; may be 0 or an identifier of a deleted message.
* @param starAmount The amount of received Telegram Stars.
- * @param tonAmount The amount of received Toncoins; in the smallest units of the cryptocurrency.
+ * @param gramAmount The amount of received TON Grams; in the smallest units of the cryptocurrency.
*/
- public MessageSuggestedPostPaid(long suggestedPostMessageId, StarAmount starAmount, long tonAmount) {
+ public MessageSuggestedPostPaid(long suggestedPostMessageId, StarAmount starAmount, long gramAmount) {
this.suggestedPostMessageId = suggestedPostMessageId;
this.starAmount = starAmount;
- this.tonAmount = tonAmount;
+ this.gramAmount = gramAmount;
}
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = 1575439273;
+ public static final int CONSTRUCTOR = -556762859;
/**
* @return this.CONSTRUCTOR
@@ -51605,7 +53301,7 @@ public static class MessageProperties extends Object {
*/
public boolean canBePinned;
/**
- * True, if the message can be replied in the same chat and forum topic using inputMessageReplyToMessage.
+ * True, if the message can be replied in the same chat and forum topic using inputMessageReplyToMessage. Ephemeral messages can be replied only by other ephemeral messages.
*/
public boolean canBeReplied;
/**
@@ -51734,7 +53430,7 @@ public MessageProperties() {
* @param canBeForwarded True, if the message can be forwarded using inputMessageForwarded or forwardMessages without copy options.
* @param canBePaid True, if the message can be paid using inputInvoiceMessage.
* @param canBePinned True, if the message can be pinned or unpinned in the chat using pinChatMessage or unpinChatMessage.
- * @param canBeReplied True, if the message can be replied in the same chat and forum topic using inputMessageReplyToMessage.
+ * @param canBeReplied True, if the message can be replied in the same chat and forum topic using inputMessageReplyToMessage. Ephemeral messages can be replied only by other ephemeral messages.
* @param canBeRepliedInAnotherChat True, if the message can be replied in another chat or forum topic using inputMessageReplyToExternalMessage.
* @param canBeSaved True, if content of the message can be saved locally.
* @param canBeSharedInStory True, if the message can be shared in a story using inputStoryAreaTypeMessage.
@@ -54098,6 +55794,68 @@ public int getConstructor() {
}
}
+ /**
+ * A sticker to be added to a sticker set.
+ */
+ public static class NewSticker extends Object {
+ /**
+ * File with the sticker; must fit in a 512x512 square. For WEBP stickers the file must be in WEBP or PNG format, which will be converted to WEBP server-side. See https://core.telegram.org/animated_stickers#technical-requirements for technical requirements.
+ */
+ public InputFile sticker;
+ /**
+ * Format of the sticker.
+ */
+ public StickerFormat format;
+ /**
+ * String with 1-20 emoji corresponding to the sticker.
+ */
+ public String emojis;
+ /**
+ * Position where the mask is placed; pass null if not specified.
+ */
+ public MaskPosition maskPosition;
+ /**
+ * List of up to 20 keywords with total length up to 64 characters, which can be used to find the sticker.
+ */
+ public String[] keywords;
+
+ /**
+ * A sticker to be added to a sticker set.
+ */
+ public NewSticker() {
+ }
+
+ /**
+ * A sticker to be added to a sticker set.
+ *
+ * @param sticker File with the sticker; must fit in a 512x512 square. For WEBP stickers the file must be in WEBP or PNG format, which will be converted to WEBP server-side. See https://core.telegram.org/animated_stickers#technical-requirements for technical requirements.
+ * @param format Format of the sticker.
+ * @param emojis String with 1-20 emoji corresponding to the sticker.
+ * @param maskPosition Position where the mask is placed; pass null if not specified.
+ * @param keywords List of up to 20 keywords with total length up to 64 characters, which can be used to find the sticker.
+ */
+ public NewSticker(InputFile sticker, StickerFormat format, String emojis, MaskPosition maskPosition, String[] keywords) {
+ this.sticker = sticker;
+ this.format = format;
+ this.emojis = emojis;
+ this.maskPosition = maskPosition;
+ this.keywords = keywords;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = -2147463841;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
/**
* Contains information about a notification.
*/
@@ -55161,7 +56919,7 @@ public int getConstructor() {
/**
* This class is an abstract base class.
- * Describes a block of an instant view for a web page.
+ * Describes a block of an instant view for a web page or a block of a rich message.
*/
public abstract static class PageBlock extends Object {
/**
@@ -61576,7 +63334,8 @@ public abstract static class PremiumFeature extends Object {
PremiumFeatureChecklists.CONSTRUCTOR,
PremiumFeaturePaidMessages.CONSTRUCTOR,
PremiumFeatureProtectPrivateChatContent.CONSTRUCTOR,
- PremiumFeatureTextComposition.CONSTRUCTOR
+ PremiumFeatureTextComposition.CONSTRUCTOR,
+ PremiumFeatureRichMessages.CONSTRUCTOR
})
public @interface Constructors {}
@@ -62293,6 +64052,31 @@ public int getConstructor() {
}
}
+ /**
+ * The ability to send rich messages.
+ */
+ public static class PremiumFeatureRichMessages extends PremiumFeature {
+
+ /**
+ * The ability to send rich messages.
+ */
+ public PremiumFeatureRichMessages() {
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = 796479291;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
/**
* Describes a promotion animation for a Premium feature.
*/
@@ -69563,6 +71347,7 @@ public abstract static class RichMessageSource extends Object {
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({
+ RichMessageSourceBlocks.CONSTRUCTOR,
RichMessageSourceMarkdown.CONSTRUCTOR,
RichMessageSourceHtml.CONSTRUCTOR
})
@@ -69581,6 +71366,44 @@ public RichMessageSource() {
}
}
+ /**
+ * A rich message defined by blocks.
+ */
+ public static class RichMessageSourceBlocks extends RichMessageSource {
+ /**
+ * Content of the message.
+ */
+ public InputPageBlock[] blocks;
+
+ /**
+ * A rich message defined by blocks.
+ */
+ public RichMessageSourceBlocks() {
+ }
+
+ /**
+ * A rich message defined by blocks.
+ *
+ * @param blocks Content of the message.
+ */
+ public RichMessageSourceBlocks(InputPageBlock[] blocks) {
+ this.blocks = blocks;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = 711854237;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
/**
* A Markdown-formatted rich message; for bots only.
*/
@@ -69589,6 +71412,10 @@ public static class RichMessageSourceMarkdown extends RichMessageSource {
* Markdown-formatted text of the message.
*/
public String text;
+ /**
+ * Media used in the message.
+ */
+ public InputRichMessageMedia[] media;
/**
* A Markdown-formatted rich message; for bots only.
@@ -69600,15 +71427,17 @@ public RichMessageSourceMarkdown() {
* A Markdown-formatted rich message; for bots only.
*
* @param text Markdown-formatted text of the message.
+ * @param media Media used in the message.
*/
- public RichMessageSourceMarkdown(String text) {
+ public RichMessageSourceMarkdown(String text, InputRichMessageMedia[] media) {
this.text = text;
+ this.media = media;
}
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = 340844665;
+ public static final int CONSTRUCTOR = 1525069830;
/**
* @return this.CONSTRUCTOR
@@ -69627,6 +71456,10 @@ public static class RichMessageSourceHtml extends RichMessageSource {
* HTML-formatted text of the message.
*/
public String text;
+ /**
+ * Media used in the message.
+ */
+ public InputRichMessageMedia[] media;
/**
* An HTML-formatted rich message; for bots only.
@@ -69638,15 +71471,17 @@ public RichMessageSourceHtml() {
* An HTML-formatted rich message; for bots only.
*
* @param text HTML-formatted text of the message.
+ * @param media Media used in the message.
*/
- public RichMessageSourceHtml(String text) {
+ public RichMessageSourceHtml(String text, InputRichMessageMedia[] media) {
this.text = text;
+ this.media = media;
}
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = 1851206828;
+ public static final int CONSTRUCTOR = -1607458262;
/**
* @return this.CONSTRUCTOR
@@ -69673,23 +71508,24 @@ public abstract static class RichText extends Object {
RichTextUnderline.CONSTRUCTOR,
RichTextStrikethrough.CONSTRUCTOR,
RichTextSpoiler.CONSTRUCTOR,
+ RichTextSubscript.CONSTRUCTOR,
+ RichTextSuperscript.CONSTRUCTOR,
+ RichTextMarked.CONSTRUCTOR,
RichTextDateTime.CONSTRUCTOR,
RichTextMention.CONSTRUCTOR,
RichTextHashtag.CONSTRUCTOR,
RichTextCashtag.CONSTRUCTOR,
+ RichTextBankCardNumber.CONSTRUCTOR,
RichTextBotCommand.CONSTRUCTOR,
RichTextFixed.CONSTRUCTOR,
RichTextMentionName.CONSTRUCTOR,
RichTextUrl.CONSTRUCTOR,
RichTextEmailAddress.CONSTRUCTOR,
- RichTextBankCardNumber.CONSTRUCTOR,
- RichTextSubscript.CONSTRUCTOR,
- RichTextSuperscript.CONSTRUCTOR,
- RichTextMarked.CONSTRUCTOR,
RichTextPhoneNumber.CONSTRUCTOR,
RichTextCustomEmoji.CONSTRUCTOR,
RichTextIcon.CONSTRUCTOR,
RichTextMathematicalExpression.CONSTRUCTOR,
+ RichTextDiff.CONSTRUCTOR,
RichTextReference.CONSTRUCTOR,
RichTextReferenceLink.CONSTRUCTOR,
RichTextAnchor.CONSTRUCTOR,
@@ -69940,89 +71776,33 @@ public int getConstructor() {
}
/**
- * A date and time.
- */
- public static class RichTextDateTime extends RichText {
- /**
- * Original text.
- */
- public RichText text;
- /**
- * Point in time (Unix timestamp) representing the date and time.
- */
- public int unixTime;
- /**
- * Date and time formatting type; may be null if none and the original text must not be changed.
- */
- @Nullable public DateTimeFormattingType formattingType;
-
- /**
- * A date and time.
- */
- public RichTextDateTime() {
- }
-
- /**
- * A date and time.
- *
- * @param text Original text.
- * @param unixTime Point in time (Unix timestamp) representing the date and time.
- * @param formattingType Date and time formatting type; may be null if none and the original text must not be changed.
- */
- public RichTextDateTime(RichText text, int unixTime, DateTimeFormattingType formattingType) {
- this.text = text;
- this.unixTime = unixTime;
- this.formattingType = formattingType;
- }
-
- /**
- * Identifier uniquely determining type of the object.
- */
- public static final int CONSTRUCTOR = 188956850;
-
- /**
- * @return this.CONSTRUCTOR
- */
- @Override
- public int getConstructor() {
- return CONSTRUCTOR;
- }
- }
-
- /**
- * A mention of a Telegram user or chat by a username.
+ * A subscript rich text.
*/
- public static class RichTextMention extends RichText {
+ public static class RichTextSubscript extends RichText {
/**
* Text.
*/
public RichText text;
- /**
- * The username.
- */
- public String username;
/**
- * A mention of a Telegram user or chat by a username.
+ * A subscript rich text.
*/
- public RichTextMention() {
+ public RichTextSubscript() {
}
/**
- * A mention of a Telegram user or chat by a username.
+ * A subscript rich text.
*
* @param text Text.
- * @param username The username.
*/
- public RichTextMention(RichText text, String username) {
+ public RichTextSubscript(RichText text) {
this.text = text;
- this.username = username;
}
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = 1982607169;
+ public static final int CONSTRUCTOR = -868197812;
/**
* @return this.CONSTRUCTOR
@@ -70034,39 +71814,33 @@ public int getConstructor() {
}
/**
- * A hashtag.
+ * A superscript rich text.
*/
- public static class RichTextHashtag extends RichText {
+ public static class RichTextSuperscript extends RichText {
/**
* Text.
*/
public RichText text;
- /**
- * The hashtag.
- */
- public String hashtag;
/**
- * A hashtag.
+ * A superscript rich text.
*/
- public RichTextHashtag() {
+ public RichTextSuperscript() {
}
/**
- * A hashtag.
+ * A superscript rich text.
*
* @param text Text.
- * @param hashtag The hashtag.
*/
- public RichTextHashtag(RichText text, String hashtag) {
+ public RichTextSuperscript(RichText text) {
this.text = text;
- this.hashtag = hashtag;
}
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = -308789407;
+ public static final int CONSTRUCTOR = -382241437;
/**
* @return this.CONSTRUCTOR
@@ -70078,39 +71852,33 @@ public int getConstructor() {
}
/**
- * A cashtag.
+ * A marked rich text.
*/
- public static class RichTextCashtag extends RichText {
+ public static class RichTextMarked extends RichText {
/**
* Text.
*/
public RichText text;
- /**
- * The cashtag.
- */
- public String cashtag;
/**
- * A cashtag.
+ * A marked rich text.
*/
- public RichTextCashtag() {
+ public RichTextMarked() {
}
/**
- * A cashtag.
+ * A marked rich text.
*
* @param text Text.
- * @param cashtag The cashtag.
*/
- public RichTextCashtag(RichText text, String cashtag) {
+ public RichTextMarked(RichText text) {
this.text = text;
- this.cashtag = cashtag;
}
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = -1293987712;
+ public static final int CONSTRUCTOR = -1271999614;
/**
* @return this.CONSTRUCTOR
@@ -70122,77 +71890,45 @@ public int getConstructor() {
}
/**
- * A bot command.
+ * A date and time.
*/
- public static class RichTextBotCommand extends RichText {
+ public static class RichTextDateTime extends RichText {
/**
- * Text.
+ * Original text.
*/
public RichText text;
/**
- * The bot command.
- */
- public String botCommand;
-
- /**
- * A bot command.
- */
- public RichTextBotCommand() {
- }
-
- /**
- * A bot command.
- *
- * @param text Text.
- * @param botCommand The bot command.
- */
- public RichTextBotCommand(RichText text, String botCommand) {
- this.text = text;
- this.botCommand = botCommand;
- }
-
- /**
- * Identifier uniquely determining type of the object.
- */
- public static final int CONSTRUCTOR = 1014029558;
-
- /**
- * @return this.CONSTRUCTOR
+ * Point in time (Unix timestamp) representing the date and time.
*/
- @Override
- public int getConstructor() {
- return CONSTRUCTOR;
- }
- }
-
- /**
- * A fixed-width rich text.
- */
- public static class RichTextFixed extends RichText {
+ public int unixTime;
/**
- * Text.
+ * Date and time formatting type; may be null if none and the original text must not be changed.
*/
- public RichText text;
+ @Nullable public DateTimeFormattingType formattingType;
/**
- * A fixed-width rich text.
+ * A date and time.
*/
- public RichTextFixed() {
+ public RichTextDateTime() {
}
/**
- * A fixed-width rich text.
+ * A date and time.
*
- * @param text Text.
+ * @param text Original text.
+ * @param unixTime Point in time (Unix timestamp) representing the date and time.
+ * @param formattingType Date and time formatting type; may be null if none and the original text must not be changed.
*/
- public RichTextFixed(RichText text) {
+ public RichTextDateTime(RichText text, int unixTime, DateTimeFormattingType formattingType) {
this.text = text;
+ this.unixTime = unixTime;
+ this.formattingType = formattingType;
}
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = -1271496249;
+ public static final int CONSTRUCTOR = 188956850;
/**
* @return this.CONSTRUCTOR
@@ -70204,39 +71940,39 @@ public int getConstructor() {
}
/**
- * A rich text that serves as a mention of a user.
+ * A mention of a Telegram user or chat by a username.
*/
- public static class RichTextMentionName extends RichText {
+ public static class RichTextMention extends RichText {
/**
* Text.
*/
public RichText text;
/**
- * Identifier of the mentioned user.
+ * The username.
*/
- public long userId;
+ public String username;
/**
- * A rich text that serves as a mention of a user.
+ * A mention of a Telegram user or chat by a username.
*/
- public RichTextMentionName() {
+ public RichTextMention() {
}
/**
- * A rich text that serves as a mention of a user.
+ * A mention of a Telegram user or chat by a username.
*
* @param text Text.
- * @param userId Identifier of the mentioned user.
+ * @param username The username.
*/
- public RichTextMentionName(RichText text, long userId) {
+ public RichTextMention(RichText text, String username) {
this.text = text;
- this.userId = userId;
+ this.username = username;
}
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = 120431556;
+ public static final int CONSTRUCTOR = 1982607169;
/**
* @return this.CONSTRUCTOR
@@ -70248,45 +71984,39 @@ public int getConstructor() {
}
/**
- * A rich text URL link.
+ * A hashtag.
*/
- public static class RichTextUrl extends RichText {
+ public static class RichTextHashtag extends RichText {
/**
* Text.
*/
public RichText text;
/**
- * URL.
- */
- public String url;
- /**
- * True, if the URL has cached instant view server-side; instant view only.
+ * The hashtag.
*/
- public boolean isCached;
+ public String hashtag;
/**
- * A rich text URL link.
+ * A hashtag.
*/
- public RichTextUrl() {
+ public RichTextHashtag() {
}
/**
- * A rich text URL link.
+ * A hashtag.
*
* @param text Text.
- * @param url URL.
- * @param isCached True, if the URL has cached instant view server-side; instant view only.
+ * @param hashtag The hashtag.
*/
- public RichTextUrl(RichText text, String url, boolean isCached) {
+ public RichTextHashtag(RichText text, String hashtag) {
this.text = text;
- this.url = url;
- this.isCached = isCached;
+ this.hashtag = hashtag;
}
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = 83939092;
+ public static final int CONSTRUCTOR = -308789407;
/**
* @return this.CONSTRUCTOR
@@ -70298,39 +72028,39 @@ public int getConstructor() {
}
/**
- * A rich text email address.
+ * A cashtag.
*/
- public static class RichTextEmailAddress extends RichText {
+ public static class RichTextCashtag extends RichText {
/**
* Text.
*/
public RichText text;
/**
- * Email address.
+ * The cashtag.
*/
- public String emailAddress;
+ public String cashtag;
/**
- * A rich text email address.
+ * A cashtag.
*/
- public RichTextEmailAddress() {
+ public RichTextCashtag() {
}
/**
- * A rich text email address.
+ * A cashtag.
*
* @param text Text.
- * @param emailAddress Email address.
+ * @param cashtag The cashtag.
*/
- public RichTextEmailAddress(RichText text, String emailAddress) {
+ public RichTextCashtag(RichText text, String cashtag) {
this.text = text;
- this.emailAddress = emailAddress;
+ this.cashtag = cashtag;
}
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = 40018679;
+ public static final int CONSTRUCTOR = -1293987712;
/**
* @return this.CONSTRUCTOR
@@ -70386,33 +72116,39 @@ public int getConstructor() {
}
/**
- * A subscript rich text.
+ * A bot command.
*/
- public static class RichTextSubscript extends RichText {
+ public static class RichTextBotCommand extends RichText {
/**
* Text.
*/
public RichText text;
+ /**
+ * The bot command.
+ */
+ public String botCommand;
/**
- * A subscript rich text.
+ * A bot command.
*/
- public RichTextSubscript() {
+ public RichTextBotCommand() {
}
/**
- * A subscript rich text.
+ * A bot command.
*
* @param text Text.
+ * @param botCommand The bot command.
*/
- public RichTextSubscript(RichText text) {
+ public RichTextBotCommand(RichText text, String botCommand) {
this.text = text;
+ this.botCommand = botCommand;
}
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = -868197812;
+ public static final int CONSTRUCTOR = 1014029558;
/**
* @return this.CONSTRUCTOR
@@ -70424,33 +72160,33 @@ public int getConstructor() {
}
/**
- * A superscript rich text.
+ * A fixed-width rich text.
*/
- public static class RichTextSuperscript extends RichText {
+ public static class RichTextFixed extends RichText {
/**
* Text.
*/
public RichText text;
/**
- * A superscript rich text.
+ * A fixed-width rich text.
*/
- public RichTextSuperscript() {
+ public RichTextFixed() {
}
/**
- * A superscript rich text.
+ * A fixed-width rich text.
*
* @param text Text.
*/
- public RichTextSuperscript(RichText text) {
+ public RichTextFixed(RichText text) {
this.text = text;
}
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = -382241437;
+ public static final int CONSTRUCTOR = -1271496249;
/**
* @return this.CONSTRUCTOR
@@ -70462,33 +72198,133 @@ public int getConstructor() {
}
/**
- * A marked rich text.
+ * A rich text that serves as a mention of a user.
*/
- public static class RichTextMarked extends RichText {
+ public static class RichTextMentionName extends RichText {
/**
* Text.
*/
public RichText text;
+ /**
+ * Identifier of the mentioned user.
+ */
+ public long userId;
/**
- * A marked rich text.
+ * A rich text that serves as a mention of a user.
*/
- public RichTextMarked() {
+ public RichTextMentionName() {
}
/**
- * A marked rich text.
+ * A rich text that serves as a mention of a user.
*
* @param text Text.
+ * @param userId Identifier of the mentioned user.
*/
- public RichTextMarked(RichText text) {
+ public RichTextMentionName(RichText text, long userId) {
this.text = text;
+ this.userId = userId;
}
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = -1271999614;
+ public static final int CONSTRUCTOR = 120431556;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * A rich text URL link.
+ */
+ public static class RichTextUrl extends RichText {
+ /**
+ * Text.
+ */
+ public RichText text;
+ /**
+ * URL.
+ */
+ public String url;
+ /**
+ * True, if the URL has cached instant view server-side; instant view only.
+ */
+ public boolean isCached;
+
+ /**
+ * A rich text URL link.
+ */
+ public RichTextUrl() {
+ }
+
+ /**
+ * A rich text URL link.
+ *
+ * @param text Text.
+ * @param url URL.
+ * @param isCached True, if the URL has cached instant view server-side; instant view only.
+ */
+ public RichTextUrl(RichText text, String url, boolean isCached) {
+ this.text = text;
+ this.url = url;
+ this.isCached = isCached;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = 83939092;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * A rich text email address.
+ */
+ public static class RichTextEmailAddress extends RichText {
+ /**
+ * Text.
+ */
+ public RichText text;
+ /**
+ * Email address.
+ */
+ public String emailAddress;
+
+ /**
+ * A rich text email address.
+ */
+ public RichTextEmailAddress() {
+ }
+
+ /**
+ * A rich text email address.
+ *
+ * @param text Text.
+ * @param emailAddress Email address.
+ */
+ public RichTextEmailAddress(RichText text, String emailAddress) {
+ this.text = text;
+ this.emailAddress = emailAddress;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = 40018679;
/**
* @return this.CONSTRUCTOR
@@ -70675,6 +72511,50 @@ public int getConstructor() {
}
}
+ /**
+ * A rich text replacing another rich text; not supported in inputRichMessage.
+ */
+ public static class RichTextDiff extends RichText {
+ /**
+ * Text.
+ */
+ public RichText text;
+ /**
+ * The old text.
+ */
+ public RichText oldText;
+
+ /**
+ * A rich text replacing another rich text; not supported in inputRichMessage.
+ */
+ public RichTextDiff() {
+ }
+
+ /**
+ * A rich text replacing another rich text; not supported in inputRichMessage.
+ *
+ * @param text Text.
+ * @param oldText The old text.
+ */
+ public RichTextDiff(RichText text, RichText oldText) {
+ this.text = text;
+ this.oldText = oldText;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = -1685283658;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
/**
* A reference.
*/
@@ -73186,7 +75066,7 @@ public abstract static class SettingsSection extends Object {
SettingsSectionInAppBrowser.CONSTRUCTOR,
SettingsSectionLanguage.CONSTRUCTOR,
SettingsSectionMyStars.CONSTRUCTOR,
- SettingsSectionMyToncoins.CONSTRUCTOR,
+ SettingsSectionMyGrams.CONSTRUCTOR,
SettingsSectionNotifications.CONSTRUCTOR,
SettingsSectionPowerSaving.CONSTRUCTOR,
SettingsSectionPremium.CONSTRUCTOR,
@@ -73629,20 +75509,20 @@ public int getConstructor() {
}
/**
- * The Toncoin balance and transaction section.
+ * The TON Gram balance and transaction section.
*/
- public static class SettingsSectionMyToncoins extends SettingsSection {
+ public static class SettingsSectionMyGrams extends SettingsSection {
/**
- * The Toncoin balance and transaction section.
+ * The TON Gram balance and transaction section.
*/
- public SettingsSectionMyToncoins() {
+ public SettingsSectionMyGrams() {
}
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = 1628818474;
+ public static final int CONSTRUCTOR = -2071203246;
/**
* @return this.CONSTRUCTOR
@@ -74468,23 +76348,23 @@ public static class StakeDiceState extends Object {
*/
public String stateHash;
/**
- * The Toncoin amount that was staked in the previous roll; in the smallest units of the currency.
+ * The amount of TON Grams staked in the previous roll; in the smallest units of the currency.
*/
- public long stakeToncoinAmount;
+ public long stakeGramAmount;
/**
- * The amounts of Toncoins that are suggested to be staked; in the smallest units of the currency.
+ * The amounts of Grams that are suggested to be staked; in the smallest units of the currency.
*/
- public long[] suggestedStakeToncoinAmounts;
+ public long[] suggestedStakeGramAmounts;
/**
* The number of rolled sixes towards the streak; 0-2.
*/
public int currentStreak;
/**
- * The number of Toncoins received by the user for each 1000 Toncoins staked if the dice outcome is 1-6 correspondingly; may be empty if the stake dice can't be sent by the current user.
+ * The number of Grams received by the user for each 1000 Grams staked if the dice outcome is 1-6 correspondingly; may be empty if the stake dice can't be sent by the current user.
*/
public int[] prizePerMille;
/**
- * The number of Toncoins received by the user for each 1000 Toncoins staked if the dice outcome is 6 three times in a row with the same stake.
+ * The number of Grams received by the user for each 1000 Grams staked if the dice outcome is 6 three times in a row with the same stake.
*/
public int streakPrizePerMille;
@@ -74498,16 +76378,16 @@ public StakeDiceState() {
* Describes state of the stake dice.
*
* @param stateHash Hash of the state to use for sending the next dice; may be empty if the stake dice can't be sent by the current user.
- * @param stakeToncoinAmount The Toncoin amount that was staked in the previous roll; in the smallest units of the currency.
- * @param suggestedStakeToncoinAmounts The amounts of Toncoins that are suggested to be staked; in the smallest units of the currency.
+ * @param stakeGramAmount The amount of TON Grams staked in the previous roll; in the smallest units of the currency.
+ * @param suggestedStakeGramAmounts The amounts of Grams that are suggested to be staked; in the smallest units of the currency.
* @param currentStreak The number of rolled sixes towards the streak; 0-2.
- * @param prizePerMille The number of Toncoins received by the user for each 1000 Toncoins staked if the dice outcome is 1-6 correspondingly; may be empty if the stake dice can't be sent by the current user.
- * @param streakPrizePerMille The number of Toncoins received by the user for each 1000 Toncoins staked if the dice outcome is 6 three times in a row with the same stake.
+ * @param prizePerMille The number of Grams received by the user for each 1000 Grams staked if the dice outcome is 1-6 correspondingly; may be empty if the stake dice can't be sent by the current user.
+ * @param streakPrizePerMille The number of Grams received by the user for each 1000 Grams staked if the dice outcome is 6 three times in a row with the same stake.
*/
- public StakeDiceState(String stateHash, long stakeToncoinAmount, long[] suggestedStakeToncoinAmounts, int currentStreak, int[] prizePerMille, int streakPrizePerMille) {
+ public StakeDiceState(String stateHash, long stakeGramAmount, long[] suggestedStakeGramAmounts, int currentStreak, int[] prizePerMille, int streakPrizePerMille) {
this.stateHash = stateHash;
- this.stakeToncoinAmount = stakeToncoinAmount;
- this.suggestedStakeToncoinAmounts = suggestedStakeToncoinAmounts;
+ this.stakeGramAmount = stakeGramAmount;
+ this.suggestedStakeGramAmounts = suggestedStakeGramAmounts;
this.currentStreak = currentStreak;
this.prizePerMille = prizePerMille;
this.streakPrizePerMille = streakPrizePerMille;
@@ -74516,7 +76396,7 @@ public StakeDiceState(String stateHash, long stakeToncoinAmount, long[] suggeste
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = 2004711564;
+ public static final int CONSTRUCTOR = 560859690;
/**
* @return this.CONSTRUCTOR
@@ -80773,12 +82653,12 @@ public int getConstructor() {
}
/**
- * The list of stories, shown in the Arvhive chat list.
+ * The list of stories, shown in the Archive chat list.
*/
public static class StoryListArchive extends StoryList {
/**
- * The list of stories, shown in the Arvhive chat list.
+ * The list of stories, shown in the Archive chat list.
*/
public StoryListArchive() {
}
@@ -81880,7 +83760,7 @@ public abstract static class SuggestedPostPrice extends Object {
@Retention(RetentionPolicy.SOURCE)
@IntDef({
SuggestedPostPriceStar.CONSTRUCTOR,
- SuggestedPostPriceTon.CONSTRUCTOR
+ SuggestedPostPriceGram.CONSTRUCTOR
})
public @interface Constructors {}
@@ -81936,33 +83816,33 @@ public int getConstructor() {
}
/**
- * Describes price of a suggested post in Toncoins.
+ * Describes price of a suggested post in TON Grams.
*/
- public static class SuggestedPostPriceTon extends SuggestedPostPrice {
+ public static class SuggestedPostPriceGram extends SuggestedPostPrice {
/**
- * The amount of 1/100 of Toncoin expected to be paid for the post; getOption("suggested_post_toncoin_cent_count_min")-getOption("suggested_post_toncoin_cent_count_max").
+ * The amount of 1/100 of Gram expected to be paid for the post; getOption("suggested_post_gram_cent_count_min")-getOption("suggested_post_gram_cent_count_max").
*/
- public long toncoinCentCount;
+ public long gramCentCount;
/**
- * Describes price of a suggested post in Toncoins.
+ * Describes price of a suggested post in TON Grams.
*/
- public SuggestedPostPriceTon() {
+ public SuggestedPostPriceGram() {
}
/**
- * Describes price of a suggested post in Toncoins.
+ * Describes price of a suggested post in TON Grams.
*
- * @param toncoinCentCount The amount of 1/100 of Toncoin expected to be paid for the post; getOption("suggested_post_toncoin_cent_count_min")-getOption("suggested_post_toncoin_cent_count_max").
+ * @param gramCentCount The amount of 1/100 of Gram expected to be paid for the post; getOption("suggested_post_gram_cent_count_min")-getOption("suggested_post_gram_cent_count_max").
*/
- public SuggestedPostPriceTon(long toncoinCentCount) {
- this.toncoinCentCount = toncoinCentCount;
+ public SuggestedPostPriceGram(long gramCentCount) {
+ this.gramCentCount = gramCentCount;
}
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = -1095222334;
+ public static final int CONSTRUCTOR = -986392265;
/**
* @return this.CONSTRUCTOR
@@ -82156,7 +84036,7 @@ public int getConstructor() {
}
/**
- * Represents a supergroup or channel with zero or more members (subscribers in the case of channels). From the point of view of the system, a channel is a special kind of a supergroup: only administrators can post and see the list of members, and posts from all administrators use the name and photo of the channel instead of individual names and profile photos. Unlike supergroups, channels can have an unlimited number of subscribers.
+ * Represents a supergroup or channel with zero or more members (subscribers in the case of channels).
*/
public static class Supergroup extends Object {
/**
@@ -82216,7 +84096,7 @@ public static class Supergroup extends Object {
*/
public boolean isSlowModeEnabled;
/**
- * True, if the supergroup is a channel.
+ * True, if the supergroup is a channel, which can have an unlimited number of subscribers, but only administrators can post there and see the list of subscribers.
*/
public boolean isChannel;
/**
@@ -82261,13 +84141,13 @@ public static class Supergroup extends Object {
@Nullable public ActiveStoryState activeStoryState;
/**
- * Represents a supergroup or channel with zero or more members (subscribers in the case of channels). From the point of view of the system, a channel is a special kind of a supergroup: only administrators can post and see the list of members, and posts from all administrators use the name and photo of the channel instead of individual names and profile photos. Unlike supergroups, channels can have an unlimited number of subscribers.
+ * Represents a supergroup or channel with zero or more members (subscribers in the case of channels).
*/
public Supergroup() {
}
/**
- * Represents a supergroup or channel with zero or more members (subscribers in the case of channels). From the point of view of the system, a channel is a special kind of a supergroup: only administrators can post and see the list of members, and posts from all administrators use the name and photo of the channel instead of individual names and profile photos. Unlike supergroups, channels can have an unlimited number of subscribers.
+ * Represents a supergroup or channel with zero or more members (subscribers in the case of channels).
*
* @param id Supergroup or channel identifier.
* @param usernames Usernames of the supergroup or channel; may be null.
@@ -82283,7 +84163,7 @@ public Supergroup() {
* @param joinToSendMessages True, if users need to join the supergroup before they can send messages. May be false only for discussion supergroups and channel direct messages groups.
* @param joinByRequest True, if all users directly joining the supergroup need to be approved by supergroup administrators.
* @param isSlowModeEnabled True, if the slow mode is enabled in the supergroup.
- * @param isChannel True, if the supergroup is a channel.
+ * @param isChannel True, if the supergroup is a channel, which can have an unlimited number of subscribers, but only administrators can post there and see the list of subscribers.
* @param isBroadcastGroup True, if the supergroup is a broadcast group, i.e. only administrators can send messages and there is no limit on the number of members.
* @param isForum True, if the supergroup is a forum with topics.
* @param isDirectMessagesGroup True, if the supergroup is a direct message group for a channel chat.
@@ -82345,6 +84225,10 @@ public static class SupergroupFullInfo extends Object {
* Chat photo; may be null if empty or unknown. If non-null, then it is the same photo as in chat.photo.
*/
@Nullable public ChatPhoto photo;
+ /**
+ * Identifier of the community to which the corresponding chat was added.
+ */
+ public long communityId;
/**
* Supergroup or channel description.
*/
@@ -82516,6 +84400,7 @@ public SupergroupFullInfo() {
* Contains full information about a supergroup or channel.
*
* @param photo Chat photo; may be null if empty or unknown. If non-null, then it is the same photo as in chat.photo.
+ * @param communityId Identifier of the community to which the corresponding chat was added.
* @param description Supergroup or channel description.
* @param memberCount Number of members in the supergroup or channel; 0 if unknown.
* @param administratorCount Number of privileged users in the supergroup or channel; 0 if unknown.
@@ -82557,8 +84442,9 @@ public SupergroupFullInfo() {
* @param upgradedFromBasicGroupId Identifier of the basic group from which supergroup was upgraded; 0 if none.
* @param upgradedFromMaxMessageId Identifier of the last message in the basic group from which supergroup was upgraded; 0 if none.
*/
- public SupergroupFullInfo(ChatPhoto photo, String description, int memberCount, int administratorCount, int restrictedCount, int bannedCount, long linkedChatId, long directMessagesChatId, int slowModeDelay, double slowModeDelayExpiresIn, boolean canEnablePaidMessages, boolean canEnablePaidReaction, boolean canGetMembers, boolean hasHiddenMembers, boolean canHideMembers, boolean canSetStickerSet, boolean canSetLocation, boolean canGetStatistics, boolean canGetRevenueStatistics, boolean canGetStarRevenueStatistics, boolean canSendGift, boolean canToggleAggressiveAntiSpam, boolean isAllHistoryAvailable, boolean canHaveSponsoredMessages, boolean hasAggressiveAntiSpamEnabled, boolean hasPaidMediaAllowed, boolean hasPinnedStories, int giftCount, int myBoostCount, int unrestrictBoostCount, long outgoingPaidMessageStarCount, long stickerSetId, long customEmojiStickerSetId, ChatLocation location, ChatInviteLink inviteLink, long guardBotUserId, BotCommands[] botCommands, BotVerification botVerification, ProfileTab mainProfileTab, long upgradedFromBasicGroupId, long upgradedFromMaxMessageId) {
+ public SupergroupFullInfo(ChatPhoto photo, long communityId, String description, int memberCount, int administratorCount, int restrictedCount, int bannedCount, long linkedChatId, long directMessagesChatId, int slowModeDelay, double slowModeDelayExpiresIn, boolean canEnablePaidMessages, boolean canEnablePaidReaction, boolean canGetMembers, boolean hasHiddenMembers, boolean canHideMembers, boolean canSetStickerSet, boolean canSetLocation, boolean canGetStatistics, boolean canGetRevenueStatistics, boolean canGetStarRevenueStatistics, boolean canSendGift, boolean canToggleAggressiveAntiSpam, boolean isAllHistoryAvailable, boolean canHaveSponsoredMessages, boolean hasAggressiveAntiSpamEnabled, boolean hasPaidMediaAllowed, boolean hasPinnedStories, int giftCount, int myBoostCount, int unrestrictBoostCount, long outgoingPaidMessageStarCount, long stickerSetId, long customEmojiStickerSetId, ChatLocation location, ChatInviteLink inviteLink, long guardBotUserId, BotCommands[] botCommands, BotVerification botVerification, ProfileTab mainProfileTab, long upgradedFromBasicGroupId, long upgradedFromMaxMessageId) {
this.photo = photo;
+ this.communityId = communityId;
this.description = description;
this.memberCount = memberCount;
this.administratorCount = administratorCount;
@@ -82604,7 +84490,7 @@ public SupergroupFullInfo(ChatPhoto photo, String description, int memberCount,
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = 1087672723;
+ public static final int CONSTRUCTOR = -1489590969;
/**
* @return this.CONSTRUCTOR
@@ -85815,113 +87701,7 @@ public int getConstructor() {
}
/**
- * A detailed statistics about Toncoins earned by the current user.
- */
- public static class TonRevenueStatistics extends Object {
- /**
- * A graph containing amount of revenue in a given day.
- */
- public StatisticalGraph revenueByDayGraph;
- /**
- * Amount of earned revenue.
- */
- public TonRevenueStatus status;
- /**
- * Current conversion rate of nanotoncoin to USD cents.
- */
- public double usdRate;
-
- /**
- * A detailed statistics about Toncoins earned by the current user.
- */
- public TonRevenueStatistics() {
- }
-
- /**
- * A detailed statistics about Toncoins earned by the current user.
- *
- * @param revenueByDayGraph A graph containing amount of revenue in a given day.
- * @param status Amount of earned revenue.
- * @param usdRate Current conversion rate of nanotoncoin to USD cents.
- */
- public TonRevenueStatistics(StatisticalGraph revenueByDayGraph, TonRevenueStatus status, double usdRate) {
- this.revenueByDayGraph = revenueByDayGraph;
- this.status = status;
- this.usdRate = usdRate;
- }
-
- /**
- * Identifier uniquely determining type of the object.
- */
- public static final int CONSTRUCTOR = 565933594;
-
- /**
- * @return this.CONSTRUCTOR
- */
- @Override
- public int getConstructor() {
- return CONSTRUCTOR;
- }
- }
-
- /**
- * Contains information about Toncoins earned by the current user.
- */
- public static class TonRevenueStatus extends Object {
- /**
- * Total Toncoin amount earned; in the smallest units of the cryptocurrency.
- */
- public long totalAmount;
- /**
- * The Toncoin amount that isn't withdrawn yet; in the smallest units of the cryptocurrency.
- */
- public long balanceAmount;
- /**
- * The Toncoin amount that is available for withdrawal; in the smallest units of the cryptocurrency.
- */
- public long availableAmount;
- /**
- * True, if Toncoins can be withdrawn.
- */
- public boolean withdrawalEnabled;
-
- /**
- * Contains information about Toncoins earned by the current user.
- */
- public TonRevenueStatus() {
- }
-
- /**
- * Contains information about Toncoins earned by the current user.
- *
- * @param totalAmount Total Toncoin amount earned; in the smallest units of the cryptocurrency.
- * @param balanceAmount The Toncoin amount that isn't withdrawn yet; in the smallest units of the cryptocurrency.
- * @param availableAmount The Toncoin amount that is available for withdrawal; in the smallest units of the cryptocurrency.
- * @param withdrawalEnabled True, if Toncoins can be withdrawn.
- */
- public TonRevenueStatus(long totalAmount, long balanceAmount, long availableAmount, boolean withdrawalEnabled) {
- this.totalAmount = totalAmount;
- this.balanceAmount = balanceAmount;
- this.availableAmount = availableAmount;
- this.withdrawalEnabled = withdrawalEnabled;
- }
-
- /**
- * Identifier uniquely determining type of the object.
- */
- public static final int CONSTRUCTOR = -1437514030;
-
- /**
- * @return this.CONSTRUCTOR
- */
- @Override
- public int getConstructor() {
- return CONSTRUCTOR;
- }
- }
-
- /**
- * Represents a transaction changing the amount of owned Toncoins.
+ * Represents a transaction changing the amount of owned TON Grams.
*/
public static class TonTransaction extends Object {
/**
@@ -85929,9 +87709,9 @@ public static class TonTransaction extends Object {
*/
public String id;
/**
- * The amount of added owned Toncoins; negative for outgoing transactions.
+ * The amount of added owned Grams, in the smallest units of the cryptocurrency; negative for outgoing transactions.
*/
- public long tonAmount;
+ public long gramAmount;
/**
* True, if the transaction is a refund of a previous transaction.
*/
@@ -85946,23 +87726,23 @@ public static class TonTransaction extends Object {
public TonTransactionType type;
/**
- * Represents a transaction changing the amount of owned Toncoins.
+ * Represents a transaction changing the amount of owned TON Grams.
*/
public TonTransaction() {
}
/**
- * Represents a transaction changing the amount of owned Toncoins.
+ * Represents a transaction changing the amount of owned TON Grams.
*
* @param id Unique identifier of the transaction.
- * @param tonAmount The amount of added owned Toncoins; negative for outgoing transactions.
+ * @param gramAmount The amount of added owned Grams, in the smallest units of the cryptocurrency; negative for outgoing transactions.
* @param isRefund True, if the transaction is a refund of a previous transaction.
* @param date Point in time (Unix timestamp) when the transaction was completed.
* @param type Type of the transaction.
*/
- public TonTransaction(String id, long tonAmount, boolean isRefund, int date, TonTransactionType type) {
+ public TonTransaction(String id, long gramAmount, boolean isRefund, int date, TonTransactionType type) {
this.id = id;
- this.tonAmount = tonAmount;
+ this.gramAmount = gramAmount;
this.isRefund = isRefund;
this.date = date;
this.type = type;
@@ -85971,7 +87751,7 @@ public TonTransaction(String id, long tonAmount, boolean isRefund, int date, Ton
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = 1562036527;
+ public static final int CONSTRUCTOR = -412943290;
/**
* @return this.CONSTRUCTOR
@@ -85984,7 +87764,7 @@ public int getConstructor() {
/**
* This class is an abstract base class.
- * Describes type of transaction with Toncoins.
+ * Describes type of transaction with TON Grams.
*/
public abstract static class TonTransactionType extends Object {
/**
@@ -86018,7 +87798,7 @@ public TonTransactionType() {
}
/**
- * The transaction is a deposit of Toncoins from Fragment.
+ * The transaction is a deposit of Grams from Fragment.
*/
public static class TonTransactionTypeFragmentDeposit extends TonTransactionType {
/**
@@ -86031,13 +87811,13 @@ public static class TonTransactionTypeFragmentDeposit extends TonTransactionType
@Nullable public Sticker sticker;
/**
- * The transaction is a deposit of Toncoins from Fragment.
+ * The transaction is a deposit of Grams from Fragment.
*/
public TonTransactionTypeFragmentDeposit() {
}
/**
- * The transaction is a deposit of Toncoins from Fragment.
+ * The transaction is a deposit of Grams from Fragment.
*
* @param isGift True, if the transaction is a gift from another user.
* @param sticker The sticker to be shown in the transaction information; may be null if unknown.
@@ -86062,7 +87842,7 @@ public int getConstructor() {
}
/**
- * The transaction is a withdrawal of earned Toncoins to Fragment.
+ * The transaction is a withdrawal of earned Grams to Fragment.
*/
public static class TonTransactionTypeFragmentWithdrawal extends TonTransactionType {
/**
@@ -86071,13 +87851,13 @@ public static class TonTransactionTypeFragmentWithdrawal extends TonTransactionT
@Nullable public RevenueWithdrawalState withdrawalState;
/**
- * The transaction is a withdrawal of earned Toncoins to Fragment.
+ * The transaction is a withdrawal of earned Grams to Fragment.
*/
public TonTransactionTypeFragmentWithdrawal() {
}
/**
- * The transaction is a withdrawal of earned Toncoins to Fragment.
+ * The transaction is a withdrawal of earned Grams to Fragment.
*
* @param withdrawalState State of the withdrawal; may be null for refunds from Fragment.
*/
@@ -86232,13 +88012,13 @@ public static class TonTransactionTypeUpgradedGiftSale extends TonTransactionTyp
*/
public UpgradedGift gift;
/**
- * The number of Toncoins received by the Telegram for each 1000 Toncoins received by the seller of the gift.
+ * The number of Grams received by the Telegram for each 1000 Grams received by the seller of the gift.
*/
public int commissionPerMille;
/**
- * The Toncoin amount that was received by the Telegram; in the smallest units of the currency.
+ * The Gram amount that was received by the Telegram; in the smallest units of the currency.
*/
- public long commissionToncoinAmount;
+ public long commissionGramAmount;
/**
* True, if the gift was sold through a purchase offer.
*/
@@ -86255,22 +88035,22 @@ public TonTransactionTypeUpgradedGiftSale() {
*
* @param userId Identifier of the user who bought the gift.
* @param gift The gift.
- * @param commissionPerMille The number of Toncoins received by the Telegram for each 1000 Toncoins received by the seller of the gift.
- * @param commissionToncoinAmount The Toncoin amount that was received by the Telegram; in the smallest units of the currency.
+ * @param commissionPerMille The number of Grams received by the Telegram for each 1000 Grams received by the seller of the gift.
+ * @param commissionGramAmount The Gram amount that was received by the Telegram; in the smallest units of the currency.
* @param viaOffer True, if the gift was sold through a purchase offer.
*/
- public TonTransactionTypeUpgradedGiftSale(long userId, UpgradedGift gift, int commissionPerMille, long commissionToncoinAmount, boolean viaOffer) {
+ public TonTransactionTypeUpgradedGiftSale(long userId, UpgradedGift gift, int commissionPerMille, long commissionGramAmount, boolean viaOffer) {
this.userId = userId;
this.gift = gift;
this.commissionPerMille = commissionPerMille;
- this.commissionToncoinAmount = commissionToncoinAmount;
+ this.commissionGramAmount = commissionGramAmount;
this.viaOffer = viaOffer;
}
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = 1162099275;
+ public static final int CONSTRUCTOR = -1711667156;
/**
* @return this.CONSTRUCTOR
@@ -86357,15 +88137,15 @@ public int getConstructor() {
}
/**
- * Represents a list of Toncoin transactions.
+ * Represents a list of TON Gram transactions.
*/
public static class TonTransactions extends Object {
/**
- * The total amount of owned Toncoins.
+ * The total amount of owned Grams, in the smallest units of the cryptocurrency.
*/
- public long tonAmount;
+ public long gramAmount;
/**
- * List of Toncoin transactions.
+ * List of Gram transactions.
*/
public TonTransaction[] transactions;
/**
@@ -86374,20 +88154,20 @@ public static class TonTransactions extends Object {
public String nextOffset;
/**
- * Represents a list of Toncoin transactions.
+ * Represents a list of TON Gram transactions.
*/
public TonTransactions() {
}
/**
- * Represents a list of Toncoin transactions.
+ * Represents a list of TON Gram transactions.
*
- * @param tonAmount The total amount of owned Toncoins.
- * @param transactions List of Toncoin transactions.
+ * @param gramAmount The total amount of owned Grams, in the smallest units of the cryptocurrency.
+ * @param transactions List of Gram transactions.
* @param nextOffset The offset for the next request. If empty, then there are no more results.
*/
- public TonTransactions(long tonAmount, TonTransaction[] transactions, String nextOffset) {
- this.tonAmount = tonAmount;
+ public TonTransactions(long gramAmount, TonTransaction[] transactions, String nextOffset) {
+ this.gramAmount = gramAmount;
this.transactions = transactions;
this.nextOffset = nextOffset;
}
@@ -86395,7 +88175,7 @@ public TonTransactions(long tonAmount, TonTransaction[] transactions, String nex
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = 1317235021;
+ public static final int CONSTRUCTOR = -1391918595;
/**
* @return this.CONSTRUCTOR
@@ -86982,6 +88762,7 @@ public abstract static class Update extends Object {
UpdateDeleteMessages.CONSTRUCTOR,
UpdateChatAction.CONSTRUCTOR,
UpdatePendingMessage.CONSTRUCTOR,
+ UpdateCommunity.CONSTRUCTOR,
UpdateUserStatus.CONSTRUCTOR,
UpdateUser.CONSTRUCTOR,
UpdateBasicGroup.CONSTRUCTOR,
@@ -87054,10 +88835,10 @@ public abstract static class Update extends Object {
UpdateSavedMessagesTags.CONSTRUCTOR,
UpdateActiveLiveLocationMessages.CONSTRUCTOR,
UpdateOwnedStarCount.CONSTRUCTOR,
- UpdateOwnedTonCount.CONSTRUCTOR,
+ UpdateOwnedGramCount.CONSTRUCTOR,
UpdateChatRevenueAmount.CONSTRUCTOR,
UpdateStarRevenueStatus.CONSTRUCTOR,
- UpdateTonRevenueStatus.CONSTRUCTOR,
+ UpdateGramRevenueStatus.CONSTRUCTOR,
UpdateSpeechRecognitionTrial.CONSTRUCTOR,
UpdateGroupCallMessageLevels.CONSTRUCTOR,
UpdateDiceEmojis.CONSTRUCTOR,
@@ -87083,6 +88864,7 @@ public abstract static class Update extends Object {
UpdateNewPreCheckoutQuery.CONSTRUCTOR,
UpdateNewCustomEvent.CONSTRUCTOR,
UpdateNewCustomQuery.CONSTRUCTOR,
+ UpdateUserSubscription.CONSTRUCTOR,
UpdatePoll.CONSTRUCTOR,
UpdatePollAnswer.CONSTRUCTOR,
UpdateManagedBot.CONSTRUCTOR,
@@ -90456,6 +92238,44 @@ public int getConstructor() {
}
}
+ /**
+ * Some data of a community has changed. This update is guaranteed to come before the community identifier is returned to the application.
+ */
+ public static class UpdateCommunity extends Update {
+ /**
+ * New data about the community.
+ */
+ public Community community;
+
+ /**
+ * Some data of a community has changed. This update is guaranteed to come before the community identifier is returned to the application.
+ */
+ public UpdateCommunity() {
+ }
+
+ /**
+ * Some data of a community has changed. This update is guaranteed to come before the community identifier is returned to the application.
+ *
+ * @param community New data about the community.
+ */
+ public UpdateCommunity(Community community) {
+ this.community = community;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = 1429785989;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
/**
* The user went online or offline.
*/
@@ -93583,33 +95403,33 @@ public int getConstructor() {
}
/**
- * The number of Toncoins owned by the current user has changed.
+ * The number of TON Grams owned by the current user has changed.
*/
- public static class UpdateOwnedTonCount extends Update {
+ public static class UpdateOwnedGramCount extends Update {
/**
- * The new amount of owned Toncoins; in the smallest units of the cryptocurrency.
+ * The new amount of owned Grams; in the smallest units of the cryptocurrency.
*/
- public long tonAmount;
+ public long gramAmount;
/**
- * The number of Toncoins owned by the current user has changed.
+ * The number of TON Grams owned by the current user has changed.
*/
- public UpdateOwnedTonCount() {
+ public UpdateOwnedGramCount() {
}
/**
- * The number of Toncoins owned by the current user has changed.
+ * The number of TON Grams owned by the current user has changed.
*
- * @param tonAmount The new amount of owned Toncoins; in the smallest units of the cryptocurrency.
+ * @param gramAmount The new amount of owned Grams; in the smallest units of the cryptocurrency.
*/
- public UpdateOwnedTonCount(long tonAmount) {
- this.tonAmount = tonAmount;
+ public UpdateOwnedGramCount(long gramAmount) {
+ this.gramAmount = gramAmount;
}
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = -1102136345;
+ public static final int CONSTRUCTOR = -2126618955;
/**
* @return this.CONSTRUCTOR
@@ -93709,33 +95529,33 @@ public int getConstructor() {
}
/**
- * The Toncoin revenue earned by the current user has changed. If Toncoin transaction screen of the chat is opened, then getTonTransactions may be called to fetch new transactions.
+ * The TON Gram revenue earned by the current user has changed. If Gram transaction screen of the chat is opened, then getTonTransactions may be called to fetch new transactions.
*/
- public static class UpdateTonRevenueStatus extends Update {
+ public static class UpdateGramRevenueStatus extends Update {
/**
- * New Toncoin revenue status.
+ * New Gram revenue status.
*/
- public TonRevenueStatus status;
+ public GramRevenueStatus status;
/**
- * The Toncoin revenue earned by the current user has changed. If Toncoin transaction screen of the chat is opened, then getTonTransactions may be called to fetch new transactions.
+ * The TON Gram revenue earned by the current user has changed. If Gram transaction screen of the chat is opened, then getTonTransactions may be called to fetch new transactions.
*/
- public UpdateTonRevenueStatus() {
+ public UpdateGramRevenueStatus() {
}
/**
- * The Toncoin revenue earned by the current user has changed. If Toncoin transaction screen of the chat is opened, then getTonTransactions may be called to fetch new transactions.
+ * The TON Gram revenue earned by the current user has changed. If Gram transaction screen of the chat is opened, then getTonTransactions may be called to fetch new transactions.
*
- * @param status New Toncoin revenue status.
+ * @param status New Gram revenue status.
*/
- public UpdateTonRevenueStatus(TonRevenueStatus status) {
+ public UpdateGramRevenueStatus(GramRevenueStatus status) {
this.status = status;
}
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = 1832579994;
+ public static final int CONSTRUCTOR = -417460519;
/**
* @return this.CONSTRUCTOR
@@ -94984,6 +96804,68 @@ public int getConstructor() {
}
}
+ /**
+ * Subscription of a user to the bot was changed; for bots only.
+ */
+ public static class UpdateUserSubscription extends Update {
+ /**
+ * Identifier of the user.
+ */
+ public long userId;
+ /**
+ * Bot-specified subscription invoice payload.
+ */
+ public String payload;
+ /**
+ * True, if the subscription was canceled.
+ */
+ public boolean isCanceled;
+ /**
+ * True, if the subscription was restored.
+ */
+ public boolean isRestored;
+ /**
+ * True, if the payment for the subscription has failed.
+ */
+ public boolean isPaymentFailed;
+
+ /**
+ * Subscription of a user to the bot was changed; for bots only.
+ */
+ public UpdateUserSubscription() {
+ }
+
+ /**
+ * Subscription of a user to the bot was changed; for bots only.
+ *
+ * @param userId Identifier of the user.
+ * @param payload Bot-specified subscription invoice payload.
+ * @param isCanceled True, if the subscription was canceled.
+ * @param isRestored True, if the subscription was restored.
+ * @param isPaymentFailed True, if the payment for the subscription has failed.
+ */
+ public UpdateUserSubscription(long userId, String payload, boolean isCanceled, boolean isRestored, boolean isPaymentFailed) {
+ this.userId = userId;
+ this.payload = payload;
+ this.isCanceled = isCanceled;
+ this.isRestored = isRestored;
+ this.isPaymentFailed = isPaymentFailed;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = -861199645;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
/**
* A poll was updated; for bots only.
*/
@@ -97233,6 +99115,10 @@ public static class UserFullInfo extends Object {
* User profile photo visible if the main photo is hidden by privacy settings; may be null. If null and user.profilePhoto is null, then the photo is empty; otherwise, it is unknown. If non-null and both photo and personalPhoto are null, then it is the same photo as in user.profilePhoto and chat.photo. This photo isn't returned in the list of user photos.
*/
@Nullable public ChatPhoto publicPhoto;
+ /**
+ * Identifier of the community to which chat with the bot was added; for bots only.
+ */
+ public long communityId;
/**
* Block list to which the user is added; may be null if none.
*/
@@ -97358,6 +99244,7 @@ public UserFullInfo() {
* @param personalPhoto User profile photo set by the current user for the contact; may be null. If null and user.profilePhoto is null, then the photo is empty; otherwise, it is unknown. If non-null, then it is the same photo as in user.profilePhoto and chat.photo. This photo isn't returned in the list of user photos.
* @param photo User profile photo; may be null. If null and user.profilePhoto is null, then the photo is empty; otherwise, it is unknown. If non-null and personalPhoto is null, then it is the same photo as in user.profilePhoto and chat.photo.
* @param publicPhoto User profile photo visible if the main photo is hidden by privacy settings; may be null. If null and user.profilePhoto is null, then the photo is empty; otherwise, it is unknown. If non-null and both photo and personalPhoto are null, then it is the same photo as in user.profilePhoto and chat.photo. This photo isn't returned in the list of user photos.
+ * @param communityId Identifier of the community to which chat with the bot was added; for bots only.
* @param blockList Block list to which the user is added; may be null if none.
* @param canBeCalled True, if the user can be called.
* @param supportsVideoCalls True, if a video call can be created with the user.
@@ -97387,10 +99274,11 @@ public UserFullInfo() {
* @param businessInfo Information about business settings for Telegram Business accounts; may be null if none.
* @param botInfo For bots, information about the bot; may be null if the user isn't a bot.
*/
- public UserFullInfo(ChatPhoto personalPhoto, ChatPhoto photo, ChatPhoto publicPhoto, BlockList blockList, boolean canBeCalled, boolean supportsVideoCalls, boolean hasPrivateCalls, boolean hasPrivateForwards, boolean hasRestrictedVoiceAndVideoNoteMessages, boolean hasPostedToProfileStories, boolean hasSponsoredMessagesEnabled, boolean needPhoneNumberPrivacyException, boolean setChatBackground, boolean usesUnofficialApp, FormattedText bio, Birthdate birthdate, long personalChatId, int giftCount, int groupInCommonCount, long incomingPaidMessageStarCount, long outgoingPaidMessageStarCount, GiftSettings giftSettings, BotVerification botVerification, ProfileTab mainProfileTab, Audio firstProfileAudio, UserRating rating, UserRating pendingRating, int pendingRatingDate, FormattedText note, BusinessInfo businessInfo, BotInfo botInfo) {
+ public UserFullInfo(ChatPhoto personalPhoto, ChatPhoto photo, ChatPhoto publicPhoto, long communityId, BlockList blockList, boolean canBeCalled, boolean supportsVideoCalls, boolean hasPrivateCalls, boolean hasPrivateForwards, boolean hasRestrictedVoiceAndVideoNoteMessages, boolean hasPostedToProfileStories, boolean hasSponsoredMessagesEnabled, boolean needPhoneNumberPrivacyException, boolean setChatBackground, boolean usesUnofficialApp, FormattedText bio, Birthdate birthdate, long personalChatId, int giftCount, int groupInCommonCount, long incomingPaidMessageStarCount, long outgoingPaidMessageStarCount, GiftSettings giftSettings, BotVerification botVerification, ProfileTab mainProfileTab, Audio firstProfileAudio, UserRating rating, UserRating pendingRating, int pendingRatingDate, FormattedText note, BusinessInfo businessInfo, BotInfo botInfo) {
this.personalPhoto = personalPhoto;
this.photo = photo;
this.publicPhoto = publicPhoto;
+ this.communityId = communityId;
this.blockList = blockList;
this.canBeCalled = canBeCalled;
this.supportsVideoCalls = supportsVideoCalls;
@@ -97424,7 +99312,7 @@ public UserFullInfo(ChatPhoto personalPhoto, ChatPhoto photo, ChatPhoto publicPh
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = -1176229971;
+ public static final int CONSTRUCTOR = -852016007;
/**
* @return this.CONSTRUCTOR
@@ -100615,7 +102503,7 @@ public int getConstructor() {
}
/**
- * Adds multiple new members to a chat; requires canInviteUsers member right. Currently, this method is only available for supergroups and channels. This method can't be used to join a chat. Members can't be added to a channel if it has more than 200 members. Returns information about members that weren't added.
+ * Adds multiple new members to a chat; requires canInviteUsers member right. Currently, this method is available only in supergroups and channels. This method can't be used to join a chat. Members can't be added to a channel if it has more than 200 members. Returns information about members that weren't added.
*
* Returns {@link FailedToAddMembers FailedToAddMembers}
*/
@@ -100630,7 +102518,7 @@ public static class AddChatMembers extends Function {
public long[] userIds;
/**
- * Default constructor for a function, which adds multiple new members to a chat; requires canInviteUsers member right. Currently, this method is only available for supergroups and channels. This method can't be used to join a chat. Members can't be added to a channel if it has more than 200 members. Returns information about members that weren't added.
+ * Default constructor for a function, which adds multiple new members to a chat; requires canInviteUsers member right. Currently, this method is available only in supergroups and channels. This method can't be used to join a chat. Members can't be added to a channel if it has more than 200 members. Returns information about members that weren't added.
*
* Returns {@link FailedToAddMembers FailedToAddMembers}
*/
@@ -100638,7 +102526,7 @@ public AddChatMembers() {
}
/**
- * Creates a function, which adds multiple new members to a chat; requires canInviteUsers member right. Currently, this method is only available for supergroups and channels. This method can't be used to join a chat. Members can't be added to a channel if it has more than 200 members. Returns information about members that weren't added.
+ * Creates a function, which adds multiple new members to a chat; requires canInviteUsers member right. Currently, this method is available only in supergroups and channels. This method can't be used to join a chat. Members can't be added to a channel if it has more than 200 members. Returns information about members that weren't added.
*
* Returns {@link FailedToAddMembers FailedToAddMembers}
*
@@ -101543,21 +103431,9 @@ public int getConstructor() {
*/
public static class AddProfileAudio extends Function {
/**
- * The audio file to be added.
- */
- public InputFile audio;
- /**
- * Duration of the audio, in seconds; may be replaced by the server; ignored for already uploaded files.
- */
- public int duration;
- /**
- * Title of the audio; 0-64 characters; may be replaced by the server; ignored for already uploaded files.
- */
- public String title;
- /**
- * Performer of the audio; 0-64 characters, may be replaced by the server; ignored for already uploaded files.
+ * The audio to add.
*/
- public String performer;
+ public InputAudio audio;
/**
* Default constructor for a function, which adds an audio file to the beginning of the profile audio files of the current user.
@@ -101572,22 +103448,16 @@ public AddProfileAudio() {
*
* Returns {@link Ok Ok}
*
- * @param audio The audio file to be added.
- * @param duration Duration of the audio, in seconds; may be replaced by the server; ignored for already uploaded files.
- * @param title Title of the audio; 0-64 characters; may be replaced by the server; ignored for already uploaded files.
- * @param performer Performer of the audio; 0-64 characters, may be replaced by the server; ignored for already uploaded files.
+ * @param audio The audio to add.
*/
- public AddProfileAudio(InputFile audio, int duration, String title, String performer) {
+ public AddProfileAudio(InputAudio audio) {
this.audio = audio;
- this.duration = duration;
- this.title = title;
- this.performer = performer;
}
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = 919192907;
+ public static final int CONSTRUCTOR = -325879164;
/**
* @return this.CONSTRUCTOR
@@ -102033,7 +103903,7 @@ public static class AddStickerToSet extends Function {
/**
* Sticker to add to the set.
*/
- public InputSticker sticker;
+ public NewSticker sticker;
/**
* Default constructor for a function, which adds a new sticker to a set.
@@ -102052,7 +103922,7 @@ public AddStickerToSet() {
* @param name Sticker set name. The sticker set must be owned by the current user, and contain less than 200 stickers for custom emoji sticker sets and less than 120 otherwise.
* @param sticker Sticker to add to the set.
*/
- public AddStickerToSet(long userId, String name, InputSticker sticker) {
+ public AddStickerToSet(long userId, String name, NewSticker sticker) {
this.userId = userId;
this.name = name;
this.sticker = sticker;
@@ -102061,7 +103931,7 @@ public AddStickerToSet(long userId, String name, InputSticker sticker) {
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = 1457266235;
+ public static final int CONSTRUCTOR = 1291178274;
/**
* @return this.CONSTRUCTOR
@@ -102129,7 +103999,7 @@ public int getConstructor() {
}
/**
- * Adds a custom text composition style to the list of used by the user styles. May return an error with a message "TONES_SAVED_TOO_MANY" if the maximum number of added custom styles has been reached.
+ * Adds a custom text composition style to the list of used by the user styles. May return an error with a message "TONES_SAVED_TOO_MANY" if the maximum number of added custom styles getOption("added_text_composition_style_count_max") has been reached.
*
* Returns {@link Ok Ok}
*/
@@ -102140,7 +104010,7 @@ public static class AddTextCompositionStyle extends Function {
public String name;
/**
- * Default constructor for a function, which adds a custom text composition style to the list of used by the user styles. May return an error with a message "TONES_SAVED_TOO_MANY" if the maximum number of added custom styles has been reached.
+ * Default constructor for a function, which adds a custom text composition style to the list of used by the user styles. May return an error with a message "TONES_SAVED_TOO_MANY" if the maximum number of added custom styles getOption("added_text_composition_style_count_max") has been reached.
*
* Returns {@link Ok Ok}
*/
@@ -102148,7 +104018,7 @@ public AddTextCompositionStyle() {
}
/**
- * Creates a function, which adds a custom text composition style to the list of used by the user styles. May return an error with a message "TONES_SAVED_TOO_MANY" if the maximum number of added custom styles has been reached.
+ * Creates a function, which adds a custom text composition style to the list of used by the user styles. May return an error with a message "TONES_SAVED_TOO_MANY" if the maximum number of added custom styles getOption("added_text_composition_style_count_max") has been reached.
*
* Returns {@link Ok Ok}
*
@@ -105690,6 +107560,74 @@ public int getConstructor() {
}
}
+ /**
+ * Changes a rich message using an AI model. May return an error with a message "AICOMPOSE_FLOOD_PREMIUM" if Telegram Premium is required to send further requests.
+ *
+ * Returns {@link RichMessage RichMessage}
+ */
+ public static class ComposeRichMessageWithAi extends Function {
+ /**
+ * The original message.
+ */
+ public InputRichMessage message;
+ /**
+ * Pass a language code to which the text will be translated; pass an empty string if translation isn't needed. See translateText.toLanguageCode for the list of supported values.
+ */
+ public String translateToLanguageCode;
+ /**
+ * Name of the style of the resulted text; handle updateTextCompositionStyles to get the list of supported styles; pass an empty string to keep the current style of the text or if a custom prompt is used.
+ */
+ public String styleName;
+ /**
+ * Custom prompt that will be used instead of styleName; 0-getOption("text_composition_style_prompt_length_max") characters.
+ */
+ public String customPrompt;
+ /**
+ * Pass true to add emoji to the text.
+ */
+ public boolean addEmojis;
+
+ /**
+ * Default constructor for a function, which changes a rich message using an AI model. May return an error with a message "AICOMPOSE_FLOOD_PREMIUM" if Telegram Premium is required to send further requests.
+ *
+ * Returns {@link RichMessage RichMessage}
+ */
+ public ComposeRichMessageWithAi() {
+ }
+
+ /**
+ * Creates a function, which changes a rich message using an AI model. May return an error with a message "AICOMPOSE_FLOOD_PREMIUM" if Telegram Premium is required to send further requests.
+ *
+ * Returns {@link RichMessage RichMessage}
+ *
+ * @param message The original message.
+ * @param translateToLanguageCode Pass a language code to which the text will be translated; pass an empty string if translation isn't needed. See translateText.toLanguageCode for the list of supported values.
+ * @param styleName Name of the style of the resulted text; handle updateTextCompositionStyles to get the list of supported styles; pass an empty string to keep the current style of the text or if a custom prompt is used.
+ * @param customPrompt Custom prompt that will be used instead of styleName; 0-getOption("text_composition_style_prompt_length_max") characters.
+ * @param addEmojis Pass true to add emoji to the text.
+ */
+ public ComposeRichMessageWithAi(InputRichMessage message, String translateToLanguageCode, String styleName, String customPrompt, boolean addEmojis) {
+ this.message = message;
+ this.translateToLanguageCode = translateToLanguageCode;
+ this.styleName = styleName;
+ this.customPrompt = customPrompt;
+ this.addEmojis = addEmojis;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = 1974326457;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
/**
* Changes text using an AI model; must not be used in secret chats. May return an error with a message "AICOMPOSE_FLOOD_PREMIUM" if Telegram Premium is required to send further requests.
*
@@ -106029,7 +107967,7 @@ public int getConstructor() {
}
/**
- * Creates a bot which will be managed by another bot. Returns the created bot. May return an error with a message "BOT_CREATE_LIMIT_EXCEEDED" if the user already owns the maximum allowed number of bots as per premiumLimitTypeOwnedBotCount. An internal link "https://t.me/BotFather?start=deletebot" can be processed to handle the error.
+ * Creates a bot which will be managed by another bot. Returns the created bot. May return an error with a message "BOT_CREATE_LIMIT_EXCEEDED" if the user already owns the maximum allowed number of bots as per getOption("owned_bot_count_max"). An internal link "https://t.me/BotFather?start=deletebot" can be processed to handle the error.
*
* Returns {@link User User}
*/
@@ -106052,7 +107990,7 @@ public static class CreateBot extends Function {
public boolean viaLink;
/**
- * Default constructor for a function, which creates a bot which will be managed by another bot. Returns the created bot. May return an error with a message "BOT_CREATE_LIMIT_EXCEEDED" if the user already owns the maximum allowed number of bots as per premiumLimitTypeOwnedBotCount. An internal link "https://t.me/BotFather?start=deletebot" can be processed to handle the error.
+ * Default constructor for a function, which creates a bot which will be managed by another bot. Returns the created bot. May return an error with a message "BOT_CREATE_LIMIT_EXCEEDED" if the user already owns the maximum allowed number of bots as per getOption("owned_bot_count_max"). An internal link "https://t.me/BotFather?start=deletebot" can be processed to handle the error.
*
* Returns {@link User User}
*/
@@ -106060,7 +107998,7 @@ public CreateBot() {
}
/**
- * Creates a function, which creates a bot which will be managed by another bot. Returns the created bot. May return an error with a message "BOT_CREATE_LIMIT_EXCEEDED" if the user already owns the maximum allowed number of bots as per premiumLimitTypeOwnedBotCount. An internal link "https://t.me/BotFather?start=deletebot" can be processed to handle the error.
+ * Creates a function, which creates a bot which will be managed by another bot. Returns the created bot. May return an error with a message "BOT_CREATE_LIMIT_EXCEEDED" if the user already owns the maximum allowed number of bots as per getOption("owned_bot_count_max"). An internal link "https://t.me/BotFather?start=deletebot" can be processed to handle the error.
*
* Returns {@link User User}
*
@@ -106755,7 +108693,7 @@ public static class CreateNewStickerSet extends Function {
/**
* List of stickers to be added to the set; 1-200 stickers for custom emoji sticker sets, and 1-120 stickers otherwise. For TGS stickers, uploadStickerFile must be used before the sticker is shown.
*/
- public InputSticker[] stickers;
+ public NewSticker[] stickers;
/**
* Source of the sticker set; may be empty if unknown.
*/
@@ -106782,7 +108720,7 @@ public CreateNewStickerSet() {
* @param stickers List of stickers to be added to the set; 1-200 stickers for custom emoji sticker sets, and 1-120 stickers otherwise. For TGS stickers, uploadStickerFile must be used before the sticker is shown.
* @param source Source of the sticker set; may be empty if unknown.
*/
- public CreateNewStickerSet(long userId, String title, String name, StickerType stickerType, boolean needsRepainting, InputSticker[] stickers, String source) {
+ public CreateNewStickerSet(long userId, String title, String name, StickerType stickerType, boolean needsRepainting, NewSticker[] stickers, String source) {
this.userId = userId;
this.title = title;
this.name = name;
@@ -106795,7 +108733,7 @@ public CreateNewStickerSet(long userId, String title, String name, StickerType s
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = -481065727;
+ public static final int CONSTRUCTOR = -472608519;
/**
* @return this.CONSTRUCTOR
@@ -106936,6 +108874,62 @@ public int getConstructor() {
}
}
+ /**
+ * Creates a new rich message using an AI model. May return an error with a message "AICOMPOSE_FLOOD_PREMIUM" if Telegram Premium is required to send further requests.
+ *
+ * Returns {@link RichMessage RichMessage}
+ */
+ public static class CreateRichMessageWithAi extends Function {
+ /**
+ * Prompt that will be used to create the message; 0-getOption("text_composition_style_prompt_length_max") characters.
+ */
+ public String prompt;
+ /**
+ * Pass a language code in which the text will be created.
+ */
+ public String languageCode;
+ /**
+ * Pass true to add emoji to the text.
+ */
+ public boolean addEmojis;
+
+ /**
+ * Default constructor for a function, which creates a new rich message using an AI model. May return an error with a message "AICOMPOSE_FLOOD_PREMIUM" if Telegram Premium is required to send further requests.
+ *
+ * Returns {@link RichMessage RichMessage}
+ */
+ public CreateRichMessageWithAi() {
+ }
+
+ /**
+ * Creates a function, which creates a new rich message using an AI model. May return an error with a message "AICOMPOSE_FLOOD_PREMIUM" if Telegram Premium is required to send further requests.
+ *
+ * Returns {@link RichMessage RichMessage}
+ *
+ * @param prompt Prompt that will be used to create the message; 0-getOption("text_composition_style_prompt_length_max") characters.
+ * @param languageCode Pass a language code in which the text will be created.
+ * @param addEmojis Pass true to add emoji to the text.
+ */
+ public CreateRichMessageWithAi(String prompt, String languageCode, boolean addEmojis) {
+ this.prompt = prompt;
+ this.languageCode = languageCode;
+ this.addEmojis = addEmojis;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = -1458459041;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
/**
* Returns an existing chat corresponding to a known secret chat.
*
@@ -108528,6 +110522,62 @@ public int getConstructor() {
}
}
+ /**
+ * Deletes an ephemeral message; for bots only.
+ *
+ * Returns {@link Ok Ok}
+ */
+ public static class DeleteEphemeralMessage extends Function {
+ /**
+ * Chat identifier.
+ */
+ public long chatId;
+ /**
+ * Identifier of the user who received the message.
+ */
+ public long receiverUserId;
+ /**
+ * Identifiers of the message to be deleted.
+ */
+ public int ephemeralMessageId;
+
+ /**
+ * Default constructor for a function, which deletes an ephemeral message; for bots only.
+ *
+ * Returns {@link Ok Ok}
+ */
+ public DeleteEphemeralMessage() {
+ }
+
+ /**
+ * Creates a function, which deletes an ephemeral message; for bots only.
+ *
+ * Returns {@link Ok Ok}
+ *
+ * @param chatId Chat identifier.
+ * @param receiverUserId Identifier of the user who received the message.
+ * @param ephemeralMessageId Identifiers of the message to be deleted.
+ */
+ public DeleteEphemeralMessage(long chatId, long receiverUserId, int ephemeralMessageId) {
+ this.chatId = chatId;
+ this.receiverUserId = receiverUserId;
+ this.ephemeralMessageId = ephemeralMessageId;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = -866146266;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
/**
* Deletes a file from the TDLib file cache.
*
@@ -110698,7 +112748,7 @@ public int getConstructor() {
}
/**
- * Edits a non-primary invite link for a chat. Available for basic groups, supergroups, and channels. If the link creates a subscription, then expirationDate, memberLimit and createsJoinRequest must not be used. Requires administrator privileges and canInviteUsers right in the chat for own links and owner privileges for other links.
+ * Edits a non-primary invite link for a chat. Available in basic groups, supergroups, and channels. If the link creates a subscription, then expirationDate, memberLimit and createsJoinRequest must not be used. Requires administrator privileges and canInviteUsers right in the chat for own links and owner privileges for other links.
*
* Returns {@link ChatInviteLink ChatInviteLink}
*/
@@ -110729,7 +112779,7 @@ public static class EditChatInviteLink extends Function {
public boolean createsJoinRequest;
/**
- * Default constructor for a function, which edits a non-primary invite link for a chat. Available for basic groups, supergroups, and channels. If the link creates a subscription, then expirationDate, memberLimit and createsJoinRequest must not be used. Requires administrator privileges and canInviteUsers right in the chat for own links and owner privileges for other links.
+ * Default constructor for a function, which edits a non-primary invite link for a chat. Available in basic groups, supergroups, and channels. If the link creates a subscription, then expirationDate, memberLimit and createsJoinRequest must not be used. Requires administrator privileges and canInviteUsers right in the chat for own links and owner privileges for other links.
*
* Returns {@link ChatInviteLink ChatInviteLink}
*/
@@ -110737,7 +112787,7 @@ public EditChatInviteLink() {
}
/**
- * Creates a function, which edits a non-primary invite link for a chat. Available for basic groups, supergroups, and channels. If the link creates a subscription, then expirationDate, memberLimit and createsJoinRequest must not be used. Requires administrator privileges and canInviteUsers right in the chat for own links and owner privileges for other links.
+ * Creates a function, which edits a non-primary invite link for a chat. Available in basic groups, supergroups, and channels. If the link creates a subscription, then expirationDate, memberLimit and createsJoinRequest must not be used. Requires administrator privileges and canInviteUsers right in the chat for own links and owner privileges for other links.
*
* Returns {@link ChatInviteLink ChatInviteLink}
*
@@ -110871,6 +112921,74 @@ public int getConstructor() {
}
}
+ /**
+ * Edits the text, caption or reply markup of an ephemeral message sent by the bot; for bots only.
+ *
+ * Returns {@link Ok Ok}
+ */
+ public static class EditEphemeralMessage extends Function {
+ /**
+ * The chat the message belongs to.
+ */
+ public long chatId;
+ /**
+ * Identifier of the user who received the message.
+ */
+ public long receiverUserId;
+ /**
+ * Identifier of the ephemeral message.
+ */
+ public int ephemeralMessageId;
+ /**
+ * The new message reply markup; pass null if none.
+ */
+ public ReplyMarkup replyMarkup;
+ /**
+ * New content of the message; pass null to edit only reply markup. Must be one of the following types: inputMessageText, inputMessageAnimation, inputMessageAudio, inputMessageDocument, inputMessagePhoto, inputMessageSticker, inputMessageVideo, inputMessageVideoNote, inputMessageVoiceNote.
+ */
+ public InputMessageContent inputMessageContent;
+
+ /**
+ * Default constructor for a function, which edits the text, caption or reply markup of an ephemeral message sent by the bot; for bots only.
+ *
+ * Returns {@link Ok Ok}
+ */
+ public EditEphemeralMessage() {
+ }
+
+ /**
+ * Creates a function, which edits the text, caption or reply markup of an ephemeral message sent by the bot; for bots only.
+ *
+ * Returns {@link Ok Ok}
+ *
+ * @param chatId The chat the message belongs to.
+ * @param receiverUserId Identifier of the user who received the message.
+ * @param ephemeralMessageId Identifier of the ephemeral message.
+ * @param replyMarkup The new message reply markup; pass null if none.
+ * @param inputMessageContent New content of the message; pass null to edit only reply markup. Must be one of the following types: inputMessageText, inputMessageAnimation, inputMessageAudio, inputMessageDocument, inputMessagePhoto, inputMessageSticker, inputMessageVideo, inputMessageVideoNote, inputMessageVoiceNote.
+ */
+ public EditEphemeralMessage(long chatId, long receiverUserId, int ephemeralMessageId, ReplyMarkup replyMarkup, InputMessageContent inputMessageContent) {
+ this.chatId = chatId;
+ this.receiverUserId = receiverUserId;
+ this.ephemeralMessageId = ephemeralMessageId;
+ this.replyMarkup = replyMarkup;
+ this.inputMessageContent = inputMessageContent;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = 92758875;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
/**
* Edits title and icon of a topic in a forum supergroup chat or a chat with a bot with topics; for supergroup chats requires canManageTopics administrator right unless the user is creator of the topic.
*
@@ -111164,7 +113282,7 @@ public int getConstructor() {
}
/**
- * Edits the text of an inline text or game message sent via a bot; for bots only.
+ * Edits the text of an inline text or game message sent via the bot; for bots only.
*
* Returns {@link Ok Ok}
*/
@@ -111178,12 +113296,12 @@ public static class EditInlineMessageText extends Function {
*/
public ReplyMarkup replyMarkup;
/**
- * New text content of the message. Must be of type inputMessageText or inputMessageRichMessage.
+ * New text content of the message. Must be of type inputMessageText or inputMessageRichMessage; file upload isn't supported.
*/
public InputMessageContent inputMessageContent;
/**
- * Default constructor for a function, which edits the text of an inline text or game message sent via a bot; for bots only.
+ * Default constructor for a function, which edits the text of an inline text or game message sent via the bot; for bots only.
*
* Returns {@link Ok Ok}
*/
@@ -111191,13 +113309,13 @@ public EditInlineMessageText() {
}
/**
- * Creates a function, which edits the text of an inline text or game message sent via a bot; for bots only.
+ * Creates a function, which edits the text of an inline text or game message sent via the bot; for bots only.
*
* Returns {@link Ok Ok}
*
* @param inlineMessageId Inline message identifier.
* @param replyMarkup The new message reply markup; pass null if none.
- * @param inputMessageContent New text content of the message. Must be of type inputMessageText or inputMessageRichMessage.
+ * @param inputMessageContent New text content of the message. Must be of type inputMessageText or inputMessageRichMessage; file upload isn't supported.
*/
public EditInlineMessageText(String inlineMessageId, ReplyMarkup replyMarkup, InputMessageContent inputMessageContent) {
this.inlineMessageId = inlineMessageId;
@@ -112351,6 +114469,50 @@ public int getConstructor() {
}
}
+ /**
+ * Fixes a rich message using an AI model. May return an error with a message "AICOMPOSE_FLOOD_PREMIUM" if Telegram Premium is required to send further requests.
+ *
+ * Returns {@link RichMessage RichMessage}
+ */
+ public static class FixRichMessageWithAi extends Function {
+ /**
+ * The original message.
+ */
+ public InputRichMessage message;
+
+ /**
+ * Default constructor for a function, which fixes a rich message using an AI model. May return an error with a message "AICOMPOSE_FLOOD_PREMIUM" if Telegram Premium is required to send further requests.
+ *
+ * Returns {@link RichMessage RichMessage}
+ */
+ public FixRichMessageWithAi() {
+ }
+
+ /**
+ * Creates a function, which fixes a rich message using an AI model. May return an error with a message "AICOMPOSE_FLOOD_PREMIUM" if Telegram Premium is required to send further requests.
+ *
+ * Returns {@link RichMessage RichMessage}
+ *
+ * @param message The original message.
+ */
+ public FixRichMessageWithAi(InputRichMessage message) {
+ this.message = message;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = 1644383227;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
/**
* Fixes text using an AI model; must not be used in secret chats. May return an error with a message "AICOMPOSE_FLOOD_PREMIUM" if Telegram Premium is required to send further requests.
*
@@ -114575,7 +116737,7 @@ public int getConstructor() {
}
/**
- * Returns a list of service actions taken by chat members and administrators in the last 48 hours. Available only for supergroups and channels. Requires administrator rights. Returns results in reverse chronological order (i.e., in order of decreasing eventId).
+ * Returns a list of service actions taken by chat members and administrators in the last 48 hours. Available only in supergroups and channels. Requires administrator rights. Returns results in reverse chronological order (i.e., in order of decreasing eventId).
*
* Returns {@link ChatEvents ChatEvents}
*/
@@ -114606,7 +116768,7 @@ public static class GetChatEventLog extends Function {
public long[] userIds;
/**
- * Default constructor for a function, which returns a list of service actions taken by chat members and administrators in the last 48 hours. Available only for supergroups and channels. Requires administrator rights. Returns results in reverse chronological order (i.e., in order of decreasing eventId).
+ * Default constructor for a function, which returns a list of service actions taken by chat members and administrators in the last 48 hours. Available only in supergroups and channels. Requires administrator rights. Returns results in reverse chronological order (i.e., in order of decreasing eventId).
*
* Returns {@link ChatEvents ChatEvents}
*/
@@ -114614,7 +116776,7 @@ public GetChatEventLog() {
}
/**
- * Creates a function, which returns a list of service actions taken by chat members and administrators in the last 48 hours. Available only for supergroups and channels. Requires administrator rights. Returns results in reverse chronological order (i.e., in order of decreasing eventId).
+ * Creates a function, which returns a list of service actions taken by chat members and administrators in the last 48 hours. Available only in supergroups and channels. Requires administrator rights. Returns results in reverse chronological order (i.e., in order of decreasing eventId).
*
* Returns {@link ChatEvents ChatEvents}
*
@@ -116895,7 +119057,7 @@ public int getConstructor() {
}
/**
- * Returns an emoji for the given country. Returns an empty string on failure. Can be called synchronously.
+ * Returns an emoji for the flag of the given country. Returns an empty string on failure. Can be called synchronously.
*
* Returns {@link Text Text}
*/
@@ -116906,7 +119068,7 @@ public static class GetCountryFlagEmoji extends Function {
public String countryCode;
/**
- * Default constructor for a function, which returns an emoji for the given country. Returns an empty string on failure. Can be called synchronously.
+ * Default constructor for a function, which returns an emoji for the flag of the given country. Returns an empty string on failure. Can be called synchronously.
*
* Returns {@link Text Text}
*/
@@ -116914,7 +119076,7 @@ public GetCountryFlagEmoji() {
}
/**
- * Creates a function, which returns an emoji for the given country. Returns an empty string on failure. Can be called synchronously.
+ * Creates a function, which returns an emoji for the flag of the given country. Returns an empty string on failure. Can be called synchronously.
*
* Returns {@link Text Text}
*
@@ -118774,6 +120936,94 @@ public int getConstructor() {
}
}
+ /**
+ * Returns detailed TON Gram revenue statistics of the current user.
+ *
+ * Returns {@link GramRevenueStatistics GramRevenueStatistics}
+ */
+ public static class GetGramRevenueStatistics extends Function {
+ /**
+ * Pass true if a dark theme is used by the application.
+ */
+ public boolean isDark;
+
+ /**
+ * Default constructor for a function, which returns detailed TON Gram revenue statistics of the current user.
+ *
+ * Returns {@link GramRevenueStatistics GramRevenueStatistics}
+ */
+ public GetGramRevenueStatistics() {
+ }
+
+ /**
+ * Creates a function, which returns detailed TON Gram revenue statistics of the current user.
+ *
+ * Returns {@link GramRevenueStatistics GramRevenueStatistics}
+ *
+ * @param isDark Pass true if a dark theme is used by the application.
+ */
+ public GetGramRevenueStatistics(boolean isDark) {
+ this.isDark = isDark;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = 2096609551;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
+ /**
+ * Returns a URL for TON Gram withdrawal from the current user's account. The user must have at least 10 Grams to withdraw and can withdraw up to 100000 Grams in one transaction.
+ *
+ * Returns {@link HttpUrl HttpUrl}
+ */
+ public static class GetGramWithdrawalUrl extends Function {
+ /**
+ * The 2-step verification password of the current user.
+ */
+ public String password;
+
+ /**
+ * Default constructor for a function, which returns a URL for TON Gram withdrawal from the current user's account. The user must have at least 10 Grams to withdraw and can withdraw up to 100000 Grams in one transaction.
+ *
+ * Returns {@link HttpUrl HttpUrl}
+ */
+ public GetGramWithdrawalUrl() {
+ }
+
+ /**
+ * Creates a function, which returns a URL for TON Gram withdrawal from the current user's account. The user must have at least 10 Grams to withdraw and can withdraw up to 100000 Grams in one transaction.
+ *
+ * Returns {@link HttpUrl HttpUrl}
+ *
+ * @param password The 2-step verification password of the current user.
+ */
+ public GetGramWithdrawalUrl(String password) {
+ this.password = password;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = -834016009;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
/**
* Returns greeting stickers from regular sticker sets that can be used for the start page of other users.
*
@@ -119115,6 +121365,56 @@ public int getConstructor() {
}
}
+ /**
+ * Returns an HTTPS URL of a Web App of a guard bot to open after receiving chatJoinResultGuardBotApprovalRequired.
+ *
+ * Returns {@link WebAppUrl WebAppUrl}
+ */
+ public static class GetGuardBotWebAppUrl extends Function {
+ /**
+ * Unique identifier of the join request as received in chatJoinResultGuardBotApprovalRequired.
+ */
+ public long queryId;
+ /**
+ * Parameters to use to open the Web App.
+ */
+ public WebAppOpenParameters parameters;
+
+ /**
+ * Default constructor for a function, which returns an HTTPS URL of a Web App of a guard bot to open after receiving chatJoinResultGuardBotApprovalRequired.
+ *
+ * Returns {@link WebAppUrl WebAppUrl}
+ */
+ public GetGuardBotWebAppUrl() {
+ }
+
+ /**
+ * Creates a function, which returns an HTTPS URL of a Web App of a guard bot to open after receiving chatJoinResultGuardBotApprovalRequired.
+ *
+ * Returns {@link WebAppUrl WebAppUrl}
+ *
+ * @param queryId Unique identifier of the join request as received in chatJoinResultGuardBotApprovalRequired.
+ * @param parameters Parameters to use to open the Web App.
+ */
+ public GetGuardBotWebAppUrl(long queryId, WebAppOpenParameters parameters) {
+ this.queryId = queryId;
+ this.parameters = parameters;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = 793719488;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
/**
* Returns the total number of imported contacts.
*
@@ -126124,51 +128424,7 @@ public int getConstructor() {
}
/**
- * Returns detailed Toncoin revenue statistics of the current user.
- *
- * Returns {@link TonRevenueStatistics TonRevenueStatistics}
- */
- public static class GetTonRevenueStatistics extends Function {
- /**
- * Pass true if a dark theme is used by the application.
- */
- public boolean isDark;
-
- /**
- * Default constructor for a function, which returns detailed Toncoin revenue statistics of the current user.
- *
- * Returns {@link TonRevenueStatistics TonRevenueStatistics}
- */
- public GetTonRevenueStatistics() {
- }
-
- /**
- * Creates a function, which returns detailed Toncoin revenue statistics of the current user.
- *
- * Returns {@link TonRevenueStatistics TonRevenueStatistics}
- *
- * @param isDark Pass true if a dark theme is used by the application.
- */
- public GetTonRevenueStatistics(boolean isDark) {
- this.isDark = isDark;
- }
-
- /**
- * Identifier uniquely determining type of the object.
- */
- public static final int CONSTRUCTOR = -1315591160;
-
- /**
- * @return this.CONSTRUCTOR
- */
- @Override
- public int getConstructor() {
- return CONSTRUCTOR;
- }
- }
-
- /**
- * Returns the list of Toncoin transactions of the current user.
+ * Returns the list of TON blockchain transactions of the current user.
*
* Returns {@link TonTransactions TonTransactions}
*/
@@ -126187,7 +128443,7 @@ public static class GetTonTransactions extends Function {
public int limit;
/**
- * Default constructor for a function, which returns the list of Toncoin transactions of the current user.
+ * Default constructor for a function, which returns the list of TON blockchain transactions of the current user.
*
* Returns {@link TonTransactions TonTransactions}
*/
@@ -126195,7 +128451,7 @@ public GetTonTransactions() {
}
/**
- * Creates a function, which returns the list of Toncoin transactions of the current user.
+ * Creates a function, which returns the list of TON blockchain transactions of the current user.
*
* Returns {@link TonTransactions TonTransactions}
*
@@ -126223,50 +128479,6 @@ public int getConstructor() {
}
}
- /**
- * Returns a URL for Toncoin withdrawal from the current user's account. The user must have at least 10 toncoins to withdraw and can withdraw up to 100000 Toncoins in one transaction.
- *
- * Returns {@link HttpUrl HttpUrl}
- */
- public static class GetTonWithdrawalUrl extends Function {
- /**
- * The 2-step verification password of the current user.
- */
- public String password;
-
- /**
- * Default constructor for a function, which returns a URL for Toncoin withdrawal from the current user's account. The user must have at least 10 toncoins to withdraw and can withdraw up to 100000 Toncoins in one transaction.
- *
- * Returns {@link HttpUrl HttpUrl}
- */
- public GetTonWithdrawalUrl() {
- }
-
- /**
- * Creates a function, which returns a URL for Toncoin withdrawal from the current user's account. The user must have at least 10 toncoins to withdraw and can withdraw up to 100000 Toncoins in one transaction.
- *
- * Returns {@link HttpUrl HttpUrl}
- *
- * @param password The 2-step verification password of the current user.
- */
- public GetTonWithdrawalUrl(String password) {
- this.password = password;
- }
-
- /**
- * Identifier uniquely determining type of the object.
- */
- public static final int CONSTRUCTOR = -1482519601;
-
- /**
- * @return this.CONSTRUCTOR
- */
- @Override
- public int getConstructor() {
- return CONSTRUCTOR;
- }
- }
-
/**
* Returns a list of frequently used chats.
*
@@ -130497,7 +132709,7 @@ public int getConstructor() {
}
/**
- * Readds quick reply messages which failed to add. Can be called only for messages for which messageSendingStateFailed.canRetry is true and after specified in messageSendingStateFailed.retryAfter time passed. If a message is readded, the corresponding failed to send message is deleted. Returns the sent messages in the same order as the message identifiers passed in messageIds. If a message can't be readded, null will be returned instead of the message.
+ * Re-adds quick reply messages which failed to add. Can be called only for messages for which messageSendingStateFailed.canRetry is true and after specified in messageSendingStateFailed.retryAfter time passed. If a message is re-added, the corresponding failed to send message is deleted. Returns the sent messages in the same order as the message identifiers passed in messageIds. If a message can't be readded, null will be returned instead of the message.
*
* Returns {@link QuickReplyMessages QuickReplyMessages}
*/
@@ -130507,12 +132719,12 @@ public static class ReaddQuickReplyShortcutMessages extends Function Returns {@link QuickReplyMessages QuickReplyMessages}
*/
@@ -130520,12 +132732,12 @@ public ReaddQuickReplyShortcutMessages() {
}
/**
- * Creates a function, which readds quick reply messages which failed to add. Can be called only for messages for which messageSendingStateFailed.canRetry is true and after specified in messageSendingStateFailed.retryAfter time passed. If a message is readded, the corresponding failed to send message is deleted. Returns the sent messages in the same order as the message identifiers passed in messageIds. If a message can't be readded, null will be returned instead of the message.
+ * Creates a function, which re-adds quick reply messages which failed to add. Can be called only for messages for which messageSendingStateFailed.canRetry is true and after specified in messageSendingStateFailed.retryAfter time passed. If a message is re-added, the corresponding failed to send message is deleted. Returns the sent messages in the same order as the message identifiers passed in messageIds. If a message can't be readded, null will be returned instead of the message.
*
* Returns {@link QuickReplyMessages QuickReplyMessages}
*
* @param shortcutName Name of the target shortcut.
- * @param messageIds Identifiers of the quick reply messages to readd. Message identifiers must be in a strictly increasing order.
+ * @param messageIds Identifiers of the quick reply messages to re-add. Message identifiers must be in a strictly increasing order.
*/
public ReaddQuickReplyShortcutMessages(String shortcutName, long[] messageIds) {
this.shortcutName = shortcutName;
@@ -132880,7 +135092,7 @@ public static class ReplaceStickerInSet extends Function {
/**
* Sticker to add to the set.
*/
- public InputSticker newSticker;
+ public NewSticker newSticker;
/**
* Default constructor for a function, which replaces existing sticker in a set. The function is equivalent to removeStickerFromSet, then addStickerToSet, then setStickerPositionInSet.
@@ -132900,7 +135112,7 @@ public ReplaceStickerInSet() {
* @param oldSticker Sticker to remove from the set.
* @param newSticker Sticker to add to the set.
*/
- public ReplaceStickerInSet(long userId, String name, InputFile oldSticker, InputSticker newSticker) {
+ public ReplaceStickerInSet(long userId, String name, InputFile oldSticker, NewSticker newSticker) {
this.userId = userId;
this.name = name;
this.oldSticker = oldSticker;
@@ -132910,7 +135122,7 @@ public ReplaceStickerInSet(long userId, String name, InputFile oldSticker, Input
/**
* Identifier uniquely determining type of the object.
*/
- public static final int CONSTRUCTOR = -406311399;
+ public static final int CONSTRUCTOR = 1332774129;
/**
* @return this.CONSTRUCTOR
@@ -134080,7 +136292,7 @@ public int getConstructor() {
}
/**
- * Revokes invite link for a chat. Available for basic groups, supergroups, and channels. Requires administrator privileges and canInviteUsers right in the chat for own links and owner privileges for other links. If a primary link is revoked, then additionally to the revoked link returns new primary link.
+ * Revokes invite link for a chat. Available in basic groups, supergroups, and channels. Requires administrator privileges and canInviteUsers right in the chat for own links and owner privileges for other links. If a primary link is revoked, then additionally to the revoked link returns new primary link.
*
* Returns {@link ChatInviteLinks ChatInviteLinks}
*/
@@ -134095,7 +136307,7 @@ public static class RevokeChatInviteLink extends Function {
public String inviteLink;
/**
- * Default constructor for a function, which revokes invite link for a chat. Available for basic groups, supergroups, and channels. Requires administrator privileges and canInviteUsers right in the chat for own links and owner privileges for other links. If a primary link is revoked, then additionally to the revoked link returns new primary link.
+ * Default constructor for a function, which revokes invite link for a chat. Available in basic groups, supergroups, and channels. Requires administrator privileges and canInviteUsers right in the chat for own links and owner privileges for other links. If a primary link is revoked, then additionally to the revoked link returns new primary link.
*
* Returns {@link ChatInviteLinks ChatInviteLinks}
*/
@@ -134103,7 +136315,7 @@ public RevokeChatInviteLink() {
}
/**
- * Creates a function, which revokes invite link for a chat. Available for basic groups, supergroups, and channels. Requires administrator privileges and canInviteUsers right in the chat for own links and owner privileges for other links. If a primary link is revoked, then additionally to the revoked link returns new primary link.
+ * Creates a function, which revokes invite link for a chat. Available in basic groups, supergroups, and channels. Requires administrator privileges and canInviteUsers right in the chat for own links and owner privileges for other links. If a primary link is revoked, then additionally to the revoked link returns new primary link.
*
* Returns {@link ChatInviteLinks ChatInviteLinks}
*
@@ -137101,6 +139313,98 @@ public int getConstructor() {
}
}
+ /**
+ * Sends an ephemeral message which will be received only by one bot in a chat. Currently, only ephemeral bot commands and replies to bot ephemeral messages can be sent using the method. The message is persistent across application restarts only if the message database is used. Returns the sent message.
+ *
+ * Returns {@link Message Message}
+ */
+ public static class SendEphemeralMessage extends Function {
+ /**
+ * Target chat.
+ */
+ public long chatId;
+ /**
+ * Topic in which the message will be sent; pass null if none.
+ */
+ public MessageTopic topicId;
+ /**
+ * Identifier of the user who will receive the message.
+ */
+ public long receiverUserId;
+ /**
+ * Identifier of the callback query which triggered the message; for bots only.
+ */
+ public long callbackQueryId;
+ /**
+ * Information about the message to be replied; pass null if none. The message can be an incoming ephemeral message.
+ */
+ public InputMessageReplyTo replyTo;
+ /**
+ * Non-persistent identifier, which will be returned back in messageSendingStatePending object and can be used to match sent messages and corresponding updateNewMessage updates.
+ */
+ public int sendingId;
+ /**
+ * Pass true to get a fake message instead of actually sending them.
+ */
+ public boolean onlyPreview;
+ /**
+ * Markup for replying to the message; pass null if none; for bots only.
+ */
+ public ReplyMarkup replyMarkup;
+ /**
+ * The content of the message to be sent. Must be one of the following types: inputMessageText, inputMessageAnimation, inputMessageAudio, inputMessageDocument, inputMessagePhoto, inputMessageSticker, inputMessageVideo, inputMessageVideoNote, inputMessageVoiceNote, inputMessageLocation, inputMessageVenue, inputMessageContact.
+ */
+ public InputMessageContent inputMessageContent;
+
+ /**
+ * Default constructor for a function, which sends an ephemeral message which will be received only by one bot in a chat. Currently, only ephemeral bot commands and replies to bot ephemeral messages can be sent using the method. The message is persistent across application restarts only if the message database is used. Returns the sent message.
+ *
+ * Returns {@link Message Message}
+ */
+ public SendEphemeralMessage() {
+ }
+
+ /**
+ * Creates a function, which sends an ephemeral message which will be received only by one bot in a chat. Currently, only ephemeral bot commands and replies to bot ephemeral messages can be sent using the method. The message is persistent across application restarts only if the message database is used. Returns the sent message.
+ *
+ * Returns {@link Message Message}
+ *
+ * @param chatId Target chat.
+ * @param topicId Topic in which the message will be sent; pass null if none.
+ * @param receiverUserId Identifier of the user who will receive the message.
+ * @param callbackQueryId Identifier of the callback query which triggered the message; for bots only.
+ * @param replyTo Information about the message to be replied; pass null if none. The message can be an incoming ephemeral message.
+ * @param sendingId Non-persistent identifier, which will be returned back in messageSendingStatePending object and can be used to match sent messages and corresponding updateNewMessage updates.
+ * @param onlyPreview Pass true to get a fake message instead of actually sending them.
+ * @param replyMarkup Markup for replying to the message; pass null if none; for bots only.
+ * @param inputMessageContent The content of the message to be sent. Must be one of the following types: inputMessageText, inputMessageAnimation, inputMessageAudio, inputMessageDocument, inputMessagePhoto, inputMessageSticker, inputMessageVideo, inputMessageVideoNote, inputMessageVoiceNote, inputMessageLocation, inputMessageVenue, inputMessageContact.
+ */
+ public SendEphemeralMessage(long chatId, MessageTopic topicId, long receiverUserId, long callbackQueryId, InputMessageReplyTo replyTo, int sendingId, boolean onlyPreview, ReplyMarkup replyMarkup, InputMessageContent inputMessageContent) {
+ this.chatId = chatId;
+ this.topicId = topicId;
+ this.receiverUserId = receiverUserId;
+ this.callbackQueryId = callbackQueryId;
+ this.replyTo = replyTo;
+ this.sendingId = sendingId;
+ this.onlyPreview = onlyPreview;
+ this.replyMarkup = replyMarkup;
+ this.inputMessageContent = inputMessageContent;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = 222469530;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
/**
* Sends a gift to another user or channel chat. May return an error with a message "STARGIFT_USAGE_LIMITED" if the gift was sold out.
*
@@ -137944,7 +140248,7 @@ public static class SendRichMessageDraft extends Function {
*/
public long draftId;
/**
- * Draft of the message.
+ * Draft of the message; file upload isn't supported.
*/
public InputRichMessage message;
@@ -137964,7 +140268,7 @@ public SendRichMessageDraft() {
* @param chatId Chat identifier.
* @param forumTopicId The forum topic identifier in which the message will be sent; pass 0 if none.
* @param draftId Unique identifier of the draft.
- * @param message Draft of the message.
+ * @param message Draft of the message; file upload isn't supported.
*/
public SendRichMessageDraft(long chatId, int forumTopicId, long draftId, InputRichMessage message) {
this.chatId = chatId;
@@ -141782,7 +144086,7 @@ public static class SetGiftResalePrice extends Function {
*/
public String receivedGiftId;
/**
- * The new price for the unique gift; pass null to disallow gift resale. The current user will receive getOption("gift_resale_star_earnings_per_mille") Telegram Stars for each 1000 Telegram Stars paid for the gift if the gift price is in Telegram Stars or getOption("gift_resale_ton_earnings_per_mille") Toncoins for each 1000 Toncoins paid for the gift if the gift price is in Toncoins.
+ * The new price for the unique gift; pass null to disallow gift resale. The current user will receive getOption("gift_resale_star_earnings_per_mille") Telegram Stars for each 1000 Telegram Stars paid for the gift if the gift price is in Telegram Stars or getOption("gift_resale_gram_earnings_per_mille") TON Grams for each 1000 Grams paid for the gift if the gift price is in Grams.
*/
public GiftResalePrice price;
@@ -141800,7 +144104,7 @@ public SetGiftResalePrice() {
* Returns {@link Ok Ok}
*
* @param receivedGiftId Identifier of the unique gift.
- * @param price The new price for the unique gift; pass null to disallow gift resale. The current user will receive getOption("gift_resale_star_earnings_per_mille") Telegram Stars for each 1000 Telegram Stars paid for the gift if the gift price is in Telegram Stars or getOption("gift_resale_ton_earnings_per_mille") Toncoins for each 1000 Toncoins paid for the gift if the gift price is in Toncoins.
+ * @param price The new price for the unique gift; pass null to disallow gift resale. The current user will receive getOption("gift_resale_star_earnings_per_mille") Telegram Stars for each 1000 Telegram Stars paid for the gift if the gift price is in Telegram Stars or getOption("gift_resale_gram_earnings_per_mille") TON Grams for each 1000 Grams paid for the gift if the gift price is in Grams.
*/
public SetGiftResalePrice(String receivedGiftId, GiftResalePrice price) {
this.receivedGiftId = receivedGiftId;
@@ -148932,6 +151236,68 @@ public int getConstructor() {
}
}
+ /**
+ * Extracts rich message of the given message and translates it to the given language.
+ *
+ * Returns {@link RichMessage RichMessage}
+ */
+ public static class TranslateMessageRichMessage extends Function {
+ /**
+ * Identifier of the chat to which the message belongs.
+ */
+ public long chatId;
+ /**
+ * Identifier of the message.
+ */
+ public long messageId;
+ /**
+ * Language code of the language to which the message is translated. See translateText.toLanguageCode for the list of supported values.
+ */
+ public String toLanguageCode;
+ /**
+ * Tone of the translation; see translateText.tone for the list of supported values.
+ */
+ public String tone;
+
+ /**
+ * Default constructor for a function, which extracts rich message of the given message and translates it to the given language.
+ *
+ * Returns {@link RichMessage RichMessage}
+ */
+ public TranslateMessageRichMessage() {
+ }
+
+ /**
+ * Creates a function, which extracts rich message of the given message and translates it to the given language.
+ *
+ * Returns {@link RichMessage RichMessage}
+ *
+ * @param chatId Identifier of the chat to which the message belongs.
+ * @param messageId Identifier of the message.
+ * @param toLanguageCode Language code of the language to which the message is translated. See translateText.toLanguageCode for the list of supported values.
+ * @param tone Tone of the translation; see translateText.tone for the list of supported values.
+ */
+ public TranslateMessageRichMessage(long chatId, long messageId, String toLanguageCode, String tone) {
+ this.chatId = chatId;
+ this.messageId = messageId;
+ this.toLanguageCode = toLanguageCode;
+ this.tone = tone;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = 1656749178;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
/**
* Extracts text or caption of the given message and translates it to the given language; must not be used in secret chats. If the current user is a Telegram Premium user, then text formatting is preserved.
*
@@ -148994,6 +151360,62 @@ public int getConstructor() {
}
}
+ /**
+ * Translates a rich message to the given language.
+ *
+ * Returns {@link RichMessage RichMessage}
+ */
+ public static class TranslateRichMessage extends Function {
+ /**
+ * Rich message to translate.
+ */
+ public InputRichMessage message;
+ /**
+ * Language code of the language to which the message is translated. See translateText.toLanguageCode for the list of supported values.
+ */
+ public String toLanguageCode;
+ /**
+ * Tone of the translation; see translateText.tone for the list of supported values.
+ */
+ public String tone;
+
+ /**
+ * Default constructor for a function, which translates a rich message to the given language.
+ *
+ * Returns {@link RichMessage RichMessage}
+ */
+ public TranslateRichMessage() {
+ }
+
+ /**
+ * Creates a function, which translates a rich message to the given language.
+ *
+ * Returns {@link RichMessage RichMessage}
+ *
+ * @param message Rich message to translate.
+ * @param toLanguageCode Language code of the language to which the message is translated. See translateText.toLanguageCode for the list of supported values.
+ * @param tone Tone of the translation; see translateText.tone for the list of supported values.
+ */
+ public TranslateRichMessage(InputRichMessage message, String toLanguageCode, String tone) {
+ this.message = message;
+ this.toLanguageCode = toLanguageCode;
+ this.tone = tone;
+ }
+
+ /**
+ * Identifier uniquely determining type of the object.
+ */
+ public static final int CONSTRUCTOR = 2127851321;
+
+ /**
+ * @return this.CONSTRUCTOR
+ */
+ @Override
+ public int getConstructor() {
+ return CONSTRUCTOR;
+ }
+ }
+
/**
* Translates a text to the given language; must not be used in secret chats. If the current user is a Telegram Premium user, then text formatting is preserved.
*
diff --git a/data/src/official/java/org/monogram/data/compat/MessageAiCompat.kt b/data/src/official/java/org/monogram/data/compat/MessageAiCompat.kt
new file mode 100644
index 000000000..987125b91
--- /dev/null
+++ b/data/src/official/java/org/monogram/data/compat/MessageAiCompat.kt
@@ -0,0 +1,27 @@
+package org.monogram.data.compat
+
+import org.drinkless.tdlib.TdApi
+import org.monogram.data.gateway.TelegramGateway
+import org.monogram.data.mapper.toDomainRichMessage
+import org.monogram.domain.models.webapp.toEditorPlainText
+import org.monogram.domain.repository.FormattedTextResult
+
+internal fun isPromptBasedAiSupported(): Boolean = true
+
+internal suspend fun TelegramGateway.generateTextWithAi(
+ prompt: String,
+ languageCode: String,
+ addEmojis: Boolean
+): FormattedTextResult? {
+ if (prompt.isBlank()) return null
+
+ return when (val result =
+ execute(TdApi.CreateRichMessageWithAi(prompt, languageCode, addEmojis))) {
+ is TdApi.RichMessage -> {
+ val text = result.toDomainRichMessage(0L, 0L).blocks.toEditorPlainText()
+ if (text.isBlank()) null else FormattedTextResult(text = text, entities = emptyList())
+ }
+
+ else -> null
+ }
+}
diff --git a/data/src/official/java/org/monogram/data/compat/MessageInputCompat.kt b/data/src/official/java/org/monogram/data/compat/MessageInputCompat.kt
index a12abb9c0..40901619d 100644
--- a/data/src/official/java/org/monogram/data/compat/MessageInputCompat.kt
+++ b/data/src/official/java/org/monogram/data/compat/MessageInputCompat.kt
@@ -15,3 +15,21 @@ internal fun buildInputDocument(
internal fun buildInputAnimation(file: TdApi.InputFile): TdApi.InputAnimation =
TdApi.InputAnimation(file, null, intArrayOf(), 0, 0, 0)
+
+internal fun buildInputSticker(
+ file: TdApi.InputFile,
+ width: Int,
+ height: Int
+): TdApi.InputSticker = TdApi.InputSticker(file, null, width, height)
+
+internal fun buildInputVideoNote(
+ file: TdApi.InputFile,
+ duration: Int,
+ length: Int
+): TdApi.InputVideoNote = TdApi.InputVideoNote(file, null, duration, length)
+
+internal fun buildInputVoiceNote(
+ file: TdApi.InputFile,
+ duration: Int,
+ waveform: ByteArray
+): TdApi.InputVoiceNote = TdApi.InputVoiceNote(file, duration, waveform)
diff --git a/data/src/official/java/org/monogram/data/compat/MessageValueCompat.kt b/data/src/official/java/org/monogram/data/compat/MessageValueCompat.kt
new file mode 100644
index 000000000..78341f400
--- /dev/null
+++ b/data/src/official/java/org/monogram/data/compat/MessageValueCompat.kt
@@ -0,0 +1,30 @@
+package org.monogram.data.compat
+
+import org.drinkless.tdlib.TdApi
+
+internal fun buildBotCommand(command: String, description: String): TdApi.BotCommand =
+ TdApi.BotCommand(command, description, false)
+
+internal fun buildRichMessageSourceMarkdown(text: String): TdApi.RichMessageSourceMarkdown =
+ TdApi.RichMessageSourceMarkdown(text, emptyArray())
+
+internal fun buildRichMessageSourceHtml(text: String): TdApi.RichMessageSourceHtml =
+ TdApi.RichMessageSourceHtml(text, emptyArray())
+
+internal fun TdApi.SuggestedPostPrice.toLegacyToncoinCentCount(): Long? = when (this) {
+ is TdApi.SuggestedPostPriceGram -> gramCentCount
+ else -> null
+}
+
+internal fun TdApi.GiftResalePrice.toLegacyToncoinCentCount(): Long? = when (this) {
+ is TdApi.GiftResalePriceGram -> gramCentCount
+ else -> null
+}
+
+internal fun TdApi.MessageStakeDice.legacyStakeTonAmount(): Long = stakeGramAmount
+
+internal fun TdApi.MessageStakeDice.legacyPrizeTonAmount(): Long = prizeGramAmount
+
+internal fun TdApi.MessageGiftedTon.legacyTonAmount(): Long = gramAmount
+
+internal fun TdApi.MessageSuggestedPostPaid.legacyTonAmount(): Long = gramAmount
diff --git a/data/src/official/jniLibs/arm64-v8a/libtdjni.so b/data/src/official/jniLibs/arm64-v8a/libtdjni.so
index c7df16789..cfefc0308 100644
Binary files a/data/src/official/jniLibs/arm64-v8a/libtdjni.so and b/data/src/official/jniLibs/arm64-v8a/libtdjni.so differ
diff --git a/data/src/official/jniLibs/armeabi-v7a/libtdjni.so b/data/src/official/jniLibs/armeabi-v7a/libtdjni.so
index 87db8bd67..e2bc6cec7 100644
Binary files a/data/src/official/jniLibs/armeabi-v7a/libtdjni.so and b/data/src/official/jniLibs/armeabi-v7a/libtdjni.so differ
diff --git a/data/src/official/jniLibs/x86/libtdjni.so b/data/src/official/jniLibs/x86/libtdjni.so
index 1eda4f436..99719f75e 100644
Binary files a/data/src/official/jniLibs/x86/libtdjni.so and b/data/src/official/jniLibs/x86/libtdjni.so differ
diff --git a/data/src/official/jniLibs/x86_64/libtdjni.so b/data/src/official/jniLibs/x86_64/libtdjni.so
index 7e867a11f..ac84d137b 100644
Binary files a/data/src/official/jniLibs/x86_64/libtdjni.so and b/data/src/official/jniLibs/x86_64/libtdjni.so differ
diff --git a/data/src/officialUnitTest/java/org/monogram/data/mapper/RichMessageMapperOfficialTest.kt b/data/src/officialUnitTest/java/org/monogram/data/mapper/RichMessageMapperOfficialTest.kt
index 983633937..965e26242 100644
--- a/data/src/officialUnitTest/java/org/monogram/data/mapper/RichMessageMapperOfficialTest.kt
+++ b/data/src/officialUnitTest/java/org/monogram/data/mapper/RichMessageMapperOfficialTest.kt
@@ -4,8 +4,11 @@ import org.drinkless.tdlib.TdApi
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
+import org.monogram.domain.models.webapp.HorizontalAlignment
import org.monogram.domain.models.webapp.PageBlock
+import org.monogram.domain.models.webapp.PageBlockTableCell
import org.monogram.domain.models.webapp.RichText
+import org.monogram.domain.models.webapp.VerticalAlignment
class RichMessageMapperOfficialTest {
@@ -22,13 +25,12 @@ class RichMessageMapperOfficialTest {
val mapped = tdRichMessage.toDomainRichMessage(
chatId = 10L,
- messageId = 20L,
- markdownSource = "# Title"
+ messageId = 20L
)
assertEquals(10L, mapped.chatId)
assertEquals(20L, mapped.messageId)
- assertEquals("# Title", mapped.markdownSource)
+ assertEquals("## Title\n\n**Body**", mapped.markdownSource)
assertTrue(mapped.isRtl)
assertEquals(false, mapped.isFull)
assertEquals(PageBlock.Header(RichText.Plain("Title")), mapped.blocks[0])
@@ -80,4 +82,144 @@ class RichMessageMapperOfficialTest {
mapped
)
}
+
+ @Test
+ fun `table preformatted block is mapped to table`() {
+ val tdBlock = TdApi.PageBlockPreformatted(
+ TdApi.RichTextPlain(
+ """
+ ┌──────────┬──────────┐
+ │ Header 1 │ Header 2 │
+ ├──────────┼──────────┤
+ │ Value 1 │ Value 2 │
+ └──────────┴──────────┘
+ """.trimIndent()
+ ),
+ "table"
+ )
+
+ val mapped = tdBlock.toPageBlock()
+
+ assertEquals(
+ PageBlock.Table(
+ caption = RichText.Plain(""),
+ cells = listOf(
+ listOf(
+ PageBlockTableCell(
+ text = RichText.Plain("Header 1"),
+ isHeader = true,
+ colspan = 1,
+ rowspan = 1,
+ align = HorizontalAlignment.LEFT,
+ valign = VerticalAlignment.TOP
+ ),
+ PageBlockTableCell(
+ text = RichText.Plain("Header 2"),
+ isHeader = true,
+ colspan = 1,
+ rowspan = 1,
+ align = HorizontalAlignment.LEFT,
+ valign = VerticalAlignment.TOP
+ )
+ ),
+ listOf(
+ PageBlockTableCell(
+ text = RichText.Plain("Value 1"),
+ isHeader = false,
+ colspan = 1,
+ rowspan = 1,
+ align = HorizontalAlignment.LEFT,
+ valign = VerticalAlignment.TOP
+ ),
+ PageBlockTableCell(
+ text = RichText.Plain("Value 2"),
+ isHeader = false,
+ colspan = 1,
+ rowspan = 1,
+ align = HorizontalAlignment.LEFT,
+ valign = VerticalAlignment.TOP
+ )
+ )
+ ),
+ isBordered = true,
+ isStriped = false
+ ),
+ mapped
+ )
+ }
+
+ @Test
+ fun `blockquote preserves nested blocks including table`() {
+ val tdBlock = TdApi.PageBlockBlockQuote(
+ arrayOf(
+ TdApi.PageBlockParagraph(TdApi.RichTextBold(TdApi.RichTextPlain("Quoted title"))),
+ TdApi.PageBlockPreformatted(
+ TdApi.RichTextPlain(
+ """
+ | A | B |
+ |---|---|
+ | 1 | 2 |
+ """.trimIndent()
+ ),
+ "table"
+ )
+ ),
+ TdApi.RichTextPlain("Author")
+ )
+
+ val mapped = tdBlock.toPageBlock()
+
+ assertEquals(
+ PageBlock.BlockQuote(
+ pageBlocks = listOf(
+ PageBlock.Paragraph(RichText.Bold(RichText.Plain("Quoted title"))),
+ PageBlock.Table(
+ caption = RichText.Plain(""),
+ cells = listOf(
+ listOf(
+ PageBlockTableCell(
+ text = RichText.Plain("A"),
+ isHeader = true,
+ colspan = 1,
+ rowspan = 1,
+ align = HorizontalAlignment.LEFT,
+ valign = VerticalAlignment.TOP
+ ),
+ PageBlockTableCell(
+ text = RichText.Plain("B"),
+ isHeader = true,
+ colspan = 1,
+ rowspan = 1,
+ align = HorizontalAlignment.LEFT,
+ valign = VerticalAlignment.TOP
+ )
+ ),
+ listOf(
+ PageBlockTableCell(
+ text = RichText.Plain("1"),
+ isHeader = false,
+ colspan = 1,
+ rowspan = 1,
+ align = HorizontalAlignment.LEFT,
+ valign = VerticalAlignment.TOP
+ ),
+ PageBlockTableCell(
+ text = RichText.Plain("2"),
+ isHeader = false,
+ colspan = 1,
+ rowspan = 1,
+ align = HorizontalAlignment.LEFT,
+ valign = VerticalAlignment.TOP
+ )
+ )
+ ),
+ isBordered = true,
+ isStriped = false
+ )
+ ),
+ credit = RichText.Plain("Author")
+ ),
+ mapped
+ )
+ }
}
diff --git a/data/src/telemt/java/org/monogram/data/compat/MessageValueCompat.kt b/data/src/telemt/java/org/monogram/data/compat/MessageValueCompat.kt
new file mode 100644
index 000000000..78341f400
--- /dev/null
+++ b/data/src/telemt/java/org/monogram/data/compat/MessageValueCompat.kt
@@ -0,0 +1,30 @@
+package org.monogram.data.compat
+
+import org.drinkless.tdlib.TdApi
+
+internal fun buildBotCommand(command: String, description: String): TdApi.BotCommand =
+ TdApi.BotCommand(command, description, false)
+
+internal fun buildRichMessageSourceMarkdown(text: String): TdApi.RichMessageSourceMarkdown =
+ TdApi.RichMessageSourceMarkdown(text, emptyArray())
+
+internal fun buildRichMessageSourceHtml(text: String): TdApi.RichMessageSourceHtml =
+ TdApi.RichMessageSourceHtml(text, emptyArray())
+
+internal fun TdApi.SuggestedPostPrice.toLegacyToncoinCentCount(): Long? = when (this) {
+ is TdApi.SuggestedPostPriceGram -> gramCentCount
+ else -> null
+}
+
+internal fun TdApi.GiftResalePrice.toLegacyToncoinCentCount(): Long? = when (this) {
+ is TdApi.GiftResalePriceGram -> gramCentCount
+ else -> null
+}
+
+internal fun TdApi.MessageStakeDice.legacyStakeTonAmount(): Long = stakeGramAmount
+
+internal fun TdApi.MessageStakeDice.legacyPrizeTonAmount(): Long = prizeGramAmount
+
+internal fun TdApi.MessageGiftedTon.legacyTonAmount(): Long = gramAmount
+
+internal fun TdApi.MessageSuggestedPostPaid.legacyTonAmount(): Long = gramAmount
diff --git a/data/src/test/java/org/monogram/data/mapper/RichMessageMarkdownTest.kt b/data/src/test/java/org/monogram/data/mapper/RichMessageMarkdownTest.kt
new file mode 100644
index 000000000..9f611432d
--- /dev/null
+++ b/data/src/test/java/org/monogram/data/mapper/RichMessageMarkdownTest.kt
@@ -0,0 +1,135 @@
+package org.monogram.data.mapper
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.monogram.domain.models.webapp.HorizontalAlignment
+import org.monogram.domain.models.webapp.PageBlock
+import org.monogram.domain.models.webapp.PageBlockListItem
+import org.monogram.domain.models.webapp.PageBlockTableCell
+import org.monogram.domain.models.webapp.RichText
+import org.monogram.domain.models.webapp.VerticalAlignment
+import org.monogram.domain.models.webapp.toEditorMarkdown
+
+class RichMessageMarkdownTest {
+
+ @Test
+ fun `converts instant view blocks to editor markdown`() {
+ val markdown = listOf(
+ PageBlock.SectionHeading(RichText.Plain("Lorem Ipsum: The Standard Dummy Text"), 1),
+ PageBlock.SectionHeading(RichText.Plain("Introduction"), 2),
+ PageBlock.Paragraph(
+ RichText.Plain(
+ "Lorem Ipsum has been the industry's standard dummy text ever since the 1500s."
+ )
+ ),
+ PageBlock.BlockQuote(
+ pageBlocks = listOf(
+ PageBlock.Paragraph(
+ RichText.Plain(
+ "Contrary to popular belief, Lorem Ipsum is not simply random text."
+ )
+ )
+ ),
+ RichText.Plain("")
+ ),
+ PageBlock.SectionHeading(RichText.Plain("Key Characteristics"), 3),
+ PageBlock.ListBlock(
+ listOf(
+ PageBlockListItem(
+ label = "•",
+ pageBlocks = listOf(
+ PageBlock.Paragraph(
+ RichText.Texts(
+ listOf(
+ RichText.Bold(RichText.Plain("Standardization")),
+ RichText.Plain(": It is the go-to placeholder.")
+ )
+ )
+ )
+ )
+ ),
+ PageBlockListItem(
+ label = "•",
+ pageBlocks = listOf(
+ PageBlock.Paragraph(
+ RichText.Texts(
+ listOf(
+ RichText.Bold(RichText.Plain("Legibility")),
+ RichText.Plain(
+ ": Unlike \"Here is some text here is some text,\" it has a more-or-less normal distribution of letters."
+ )
+ )
+ )
+ )
+ )
+ )
+ )
+ ),
+ PageBlock.Table(
+ caption = RichText.Plain(""),
+ cells = listOf(
+ listOf(
+ PageBlockTableCell(
+ text = RichText.Plain("Column A"),
+ isHeader = true,
+ colspan = 1,
+ rowspan = 1,
+ align = HorizontalAlignment.LEFT,
+ valign = VerticalAlignment.TOP
+ ),
+ PageBlockTableCell(
+ text = RichText.Plain("Column B"),
+ isHeader = true,
+ colspan = 1,
+ rowspan = 1,
+ align = HorizontalAlignment.LEFT,
+ valign = VerticalAlignment.TOP
+ )
+ ),
+ listOf(
+ PageBlockTableCell(
+ text = RichText.Plain("Alpha"),
+ isHeader = false,
+ colspan = 1,
+ rowspan = 1,
+ align = HorizontalAlignment.LEFT,
+ valign = VerticalAlignment.TOP
+ ),
+ PageBlockTableCell(
+ text = RichText.Plain("Beta"),
+ isHeader = false,
+ colspan = 1,
+ rowspan = 1,
+ align = HorizontalAlignment.LEFT,
+ valign = VerticalAlignment.TOP
+ )
+ )
+ ),
+ isBordered = true,
+ isStriped = false
+ )
+ ).toEditorMarkdown()
+
+ assertEquals(
+ """
+ # Lorem Ipsum: The Standard Dummy Text
+
+ ## Introduction
+
+ Lorem Ipsum has been the industry's standard dummy text ever since the 1500s.
+
+ > Contrary to popular belief, Lorem Ipsum is not simply random text.
+
+ ### Key Characteristics
+
+ - **Standardization**: It is the go-to placeholder.
+ - **Legibility**: Unlike "Here is some text here is some text," it has a more-or-less normal distribution of letters.
+
+ | Column A | Column B |
+ | --- | --- |
+ | Alpha | Beta |
+ """.trimIndent(),
+ markdown
+ )
+ }
+}
diff --git a/domain/src/main/java/org/monogram/domain/models/MessageModel.kt b/domain/src/main/java/org/monogram/domain/models/MessageModel.kt
index 3f54c8f59..530beab7d 100644
--- a/domain/src/main/java/org/monogram/domain/models/MessageModel.kt
+++ b/domain/src/main/java/org/monogram/domain/models/MessageModel.kt
@@ -664,6 +664,13 @@ data class WebPage(
val fileId: Int
)
+ data class VoiceNote(
+ val path: String?,
+ val duration: Int,
+ val mimeType: String?,
+ val fileId: Int
+ )
+
data class Document(
val path: String?,
val fileName: String?,
@@ -753,6 +760,9 @@ sealed interface MessageEntityType {
@Serializable
object Hashtag : MessageEntityType
+ @Serializable
+ object Cashtag : MessageEntityType
+
@Serializable
object BotCommand : MessageEntityType
@@ -768,6 +778,12 @@ sealed interface MessageEntityType {
@Serializable
object BankCardNumber : MessageEntityType
+ @Serializable
+ data class DateTime(val unixTime: Int) : MessageEntityType
+
+ @Serializable
+ data class MediaTimestamp(val mediaTimestampSeconds: Int) : MessageEntityType
+
@Serializable
data class CustomEmoji(val emojiId: Long, val path: String? = null) : MessageEntityType
diff --git a/domain/src/main/java/org/monogram/domain/models/webapp/InstantViewModel.kt b/domain/src/main/java/org/monogram/domain/models/webapp/InstantViewModel.kt
index f286c377b..45cf551ec 100644
--- a/domain/src/main/java/org/monogram/domain/models/webapp/InstantViewModel.kt
+++ b/domain/src/main/java/org/monogram/domain/models/webapp/InstantViewModel.kt
@@ -27,7 +27,7 @@ sealed interface PageBlock {
data class MathematicalExpression(val expression: String) : PageBlock
data class Anchor(val name: String) : PageBlock
data class ListBlock(val items: List) : PageBlock
- data class BlockQuote(val text: RichText, val credit: RichText) : PageBlock
+ data class BlockQuote(val pageBlocks: List, val credit: RichText) : PageBlock
data class PullQuote(val text: RichText, val credit: RichText) : PageBlock
data class AnimationBlock(
val animation: WebPage.Animation,
@@ -36,6 +36,8 @@ sealed interface PageBlock {
) : PageBlock
data class AudioBlock(val audio: WebPage.Audio, val caption: PageBlockCaption) : PageBlock
+ data class VoiceNoteBlock(val voiceNote: WebPage.VoiceNote, val caption: PageBlockCaption) :
+ PageBlock
data class PhotoBlock(val photo: WebPage.Photo, val caption: PageBlockCaption, val url: String) : PageBlock
data class VideoBlock(
val video: WebPage.Video,
@@ -113,6 +115,10 @@ sealed interface RichText {
data class Icon(val document: WebPage.Document, val width: Int, val height: Int) : RichText
data class MathematicalExpression(val expression: String) : RichText
data class Reference(val text: RichText, val anchorName: String, val url: String) : RichText
+ data class ReferenceLink(val text: RichText, val referenceName: String, val url: String) :
+ RichText
+
+ data class Diff(val text: RichText, val oldText: RichText) : RichText
data class Anchor(val name: String) : RichText
data class AnchorLink(val text: RichText, val anchorName: String, val url: String) : RichText
data class Texts(val texts: List) : RichText
diff --git a/domain/src/main/java/org/monogram/domain/models/webapp/RichMessageMarkdown.kt b/domain/src/main/java/org/monogram/domain/models/webapp/RichMessageMarkdown.kt
new file mode 100644
index 000000000..ee6222712
--- /dev/null
+++ b/domain/src/main/java/org/monogram/domain/models/webapp/RichMessageMarkdown.kt
@@ -0,0 +1,286 @@
+package org.monogram.domain.models.webapp
+
+fun List.toEditorMarkdown(): String {
+ return joinToString("\n\n") { it.toEditorMarkdown() }.trim()
+}
+
+private fun PageBlock.toEditorMarkdown(): String {
+ return when (this) {
+ is PageBlock.Title -> "# ${title.toEditorMarkdown()}"
+ is PageBlock.Subtitle -> "## ${subtitle.toEditorMarkdown()}"
+ is PageBlock.AuthorDate -> buildString {
+ append(author.toEditorMarkdown())
+ if (publishDate > 0) {
+ if (isNotBlank()) append(" • ")
+ append(publishDate)
+ }
+ }
+
+ is PageBlock.Header -> "## ${header.toEditorMarkdown()}"
+ is PageBlock.Subheader -> "### ${subheader.toEditorMarkdown()}"
+ is PageBlock.SectionHeading -> "${
+ "#".repeat(
+ size.coerceIn(
+ 1,
+ 3
+ )
+ )
+ } ${text.toEditorMarkdown()}"
+
+ is PageBlock.Kicker -> kicker.toEditorMarkdown()
+ is PageBlock.Paragraph -> text.toEditorMarkdown()
+ is PageBlock.Preformatted -> buildString {
+ append("```")
+ append(language.trim())
+ appendLine()
+ append(text.toEditorMarkdownPlainText())
+ appendLine()
+ append("```")
+ }
+
+ is PageBlock.Footer -> footer.toEditorMarkdown()
+ is PageBlock.Thinking -> text.toEditorMarkdown()
+ is PageBlock.Divider -> "---"
+ is PageBlock.MathematicalExpression -> buildString {
+ append("$$")
+ append(expression)
+ append("$$")
+ }
+
+ is PageBlock.Anchor -> ""
+ is PageBlock.ListBlock -> items.mapIndexed { index, item ->
+ val itemText = item.pageBlocks.joinToString(" ") { it.toEditorMarkdown() }
+ .replace("\n", " ")
+ .trim()
+ val marker = if (item.label.any { it.isDigit() }) "${index + 1}." else "-"
+ if (itemText.isBlank()) marker else "$marker $itemText"
+ }.joinToString("\n")
+
+ is PageBlock.BlockQuote -> quoteToMarkdown(pageBlocks, credit)
+ is PageBlock.PullQuote -> quoteToMarkdown(text, credit)
+ is PageBlock.AnimationBlock -> caption.toEditorMarkdown()
+ is PageBlock.AudioBlock -> caption.toEditorMarkdown()
+ is PageBlock.VoiceNoteBlock -> caption.toEditorMarkdown()
+ is PageBlock.PhotoBlock -> caption.toEditorMarkdown()
+ is PageBlock.VideoBlock -> caption.toEditorMarkdown()
+ is PageBlock.Cover -> cover.toEditorMarkdown()
+ is PageBlock.Embedded -> listOf(caption.toEditorMarkdown(), url)
+ .filter { it.isNotBlank() }
+ .joinToString("\n\n")
+
+ is PageBlock.EmbeddedPost -> listOf(
+ author,
+ pageBlocks.toEditorMarkdown(),
+ caption.toEditorMarkdown()
+ ).filter { it.isNotBlank() }.joinToString("\n\n")
+
+ is PageBlock.Collage -> listOf(pageBlocks.toEditorMarkdown(), caption.toEditorMarkdown())
+ .filter { it.isNotBlank() }
+ .joinToString("\n\n")
+
+ is PageBlock.Slideshow -> listOf(pageBlocks.toEditorMarkdown(), caption.toEditorMarkdown())
+ .filter { it.isNotBlank() }
+ .joinToString("\n\n")
+
+ is PageBlock.ChatLink -> if (username.isNotBlank()) {
+ formatMarkdownLink(title, "https://t.me/$username")
+ } else {
+ title
+ }
+
+ is PageBlock.Table -> buildString {
+ val captionText = caption.toEditorMarkdown()
+ if (captionText.isNotBlank()) appendLine(captionText)
+ append(cells.toEditorMarkdownTable())
+ }.trim()
+
+ is PageBlock.Details -> listOf(header.toEditorMarkdown(), pageBlocks.toEditorMarkdown())
+ .filter { it.isNotBlank() }
+ .joinToString("\n\n")
+
+ is PageBlock.RelatedArticles -> buildString {
+ val headerText = header.toEditorMarkdown()
+ if (headerText.isNotBlank()) appendLine(headerText)
+ articles.forEach { article ->
+ appendLine(
+ listOf(article.title, article.description)
+ .filter { it.isNotBlank() }
+ .joinToString(" - ")
+ )
+ }
+ }.trim()
+
+ is PageBlock.MapBlock -> caption.toEditorMarkdown()
+ is PageBlock.Unsupported -> ""
+ }
+}
+
+private fun PageBlockCaption.toEditorMarkdown(): String {
+ return listOf(text.toEditorMarkdown(), credit.toEditorMarkdown())
+ .filter { it.isNotBlank() }
+ .joinToString("\n")
+}
+
+private fun List>.toEditorMarkdownTable(): String {
+ if (isEmpty()) return ""
+
+ val rows = map { row ->
+ row.map { cell ->
+ cell.text.toEditorMarkdownPlainText()
+ .replace("\n", " ")
+ .replace("|", "\\|")
+ .trim()
+ }
+ }
+ val columnCount = rows.maxOf { it.size }
+ val normalizedRows = rows.map { row -> row + List(columnCount - row.size) { "" } }
+
+ fun renderRow(cells: List): String {
+ return cells.joinToString(" | ", prefix = "| ", postfix = " |")
+ }
+
+ return buildString {
+ appendLine(renderRow(normalizedRows.first()))
+ appendLine(renderRow(List(columnCount) { "---" }))
+ normalizedRows.drop(1).forEachIndexed { index, row ->
+ append(renderRow(row))
+ if (index != normalizedRows.drop(1).lastIndex) appendLine()
+ }
+ }
+}
+
+private fun quoteToMarkdown(pageBlocks: List, credit: RichText): String {
+ val quoteText = pageBlocks.toEditorMarkdown()
+ val quote =
+ if (quoteText.isBlank()) "" else quoteText.lineSequence().joinToString("\n") { "> $it" }
+ val quoteCredit = credit.toEditorMarkdown().takeIf { it.isNotBlank() }
+ ?.let { "\n> — $it" }
+ .orEmpty()
+ return (quote + quoteCredit).trim()
+}
+
+private fun quoteToMarkdown(text: RichText, credit: RichText): String {
+ val quoteText = text.toEditorMarkdown()
+ val quote =
+ if (quoteText.isBlank()) "" else quoteText.lineSequence().joinToString("\n") { "> $it" }
+ val quoteCredit = credit.toEditorMarkdown().takeIf { it.isNotBlank() }
+ ?.let { "\n> — $it" }
+ .orEmpty()
+ return (quote + quoteCredit).trim()
+}
+
+private fun RichText.toEditorMarkdown(): String {
+ return when (this) {
+ is RichText.Plain -> escapeMarkdownText(text)
+ is RichText.Bold -> "**${text.toEditorMarkdown()}**"
+ is RichText.Italic -> "*${text.toEditorMarkdown()}*"
+ is RichText.Underline -> "__${text.toEditorMarkdown()}__"
+ is RichText.Strikethrough -> "~~${text.toEditorMarkdown()}~~"
+ is RichText.Spoiler -> "||${text.toEditorMarkdown()}||"
+ is RichText.DateTime -> text.toEditorMarkdown()
+ is RichText.Mention -> text.toEditorMarkdown()
+ is RichText.Hashtag -> text.toEditorMarkdown()
+ is RichText.Cashtag -> text.toEditorMarkdown()
+ is RichText.BotCommand -> text.toEditorMarkdown()
+ is RichText.Fixed -> "`" + text.toEditorMarkdownPlainText().replace("`", "\\`") + "`"
+ is RichText.MentionName -> text.toEditorMarkdown()
+ is RichText.Url -> if (text.toEditorMarkdown().isBlank()) {
+ formatMarkdownLink(url, url)
+ } else {
+ formatMarkdownLink(text.toEditorMarkdown(), url)
+ }
+
+ is RichText.EmailAddress -> formatMarkdownLink(
+ text.toEditorMarkdown(),
+ "mailto:$emailAddress"
+ )
+
+ is RichText.BankCardNumber -> text.toEditorMarkdown()
+ is RichText.Subscript -> text.toEditorMarkdown()
+ is RichText.Superscript -> text.toEditorMarkdown()
+ is RichText.Marked -> text.toEditorMarkdown()
+ is RichText.PhoneNumber -> text.toEditorMarkdown()
+ is RichText.CustomEmoji -> escapeMarkdownText(alternativeText)
+ is RichText.Icon -> ""
+ is RichText.MathematicalExpression -> buildString {
+ append('$')
+ append(expression)
+ append('$')
+ }
+
+ is RichText.Reference -> text.toEditorMarkdown()
+ is RichText.ReferenceLink -> if (url.isBlank()) {
+ text.toEditorMarkdown()
+ } else {
+ formatMarkdownLink(text.toEditorMarkdown(), url)
+ }
+
+ is RichText.Diff -> text.toEditorMarkdown()
+ is RichText.Anchor -> ""
+ is RichText.AnchorLink -> if (url.isBlank()) {
+ text.toEditorMarkdown()
+ } else {
+ formatMarkdownLink(text.toEditorMarkdown(), url)
+ }
+
+ is RichText.Texts -> texts.joinToString("") { it.toEditorMarkdown() }
+ }
+}
+
+private fun RichText.toEditorMarkdownPlainText(): String {
+ return when (this) {
+ is RichText.Plain -> text
+ is RichText.Bold -> text.toEditorMarkdownPlainText()
+ is RichText.Italic -> text.toEditorMarkdownPlainText()
+ is RichText.Underline -> text.toEditorMarkdownPlainText()
+ is RichText.Strikethrough -> text.toEditorMarkdownPlainText()
+ is RichText.Spoiler -> text.toEditorMarkdownPlainText()
+ is RichText.DateTime -> text.toEditorMarkdownPlainText()
+ is RichText.Mention -> text.toEditorMarkdownPlainText()
+ is RichText.Hashtag -> text.toEditorMarkdownPlainText()
+ is RichText.Cashtag -> text.toEditorMarkdownPlainText()
+ is RichText.BotCommand -> text.toEditorMarkdownPlainText()
+ is RichText.Fixed -> text.toEditorMarkdownPlainText()
+ is RichText.MentionName -> text.toEditorMarkdownPlainText()
+ is RichText.Url -> text.toEditorMarkdownPlainText()
+ is RichText.EmailAddress -> text.toEditorMarkdownPlainText()
+ is RichText.BankCardNumber -> text.toEditorMarkdownPlainText()
+ is RichText.Subscript -> text.toEditorMarkdownPlainText()
+ is RichText.Superscript -> text.toEditorMarkdownPlainText()
+ is RichText.Marked -> text.toEditorMarkdownPlainText()
+ is RichText.PhoneNumber -> text.toEditorMarkdownPlainText()
+ is RichText.CustomEmoji -> alternativeText
+ is RichText.Icon -> ""
+ is RichText.MathematicalExpression -> expression
+ is RichText.Reference -> text.toEditorMarkdownPlainText()
+ is RichText.ReferenceLink -> text.toEditorMarkdownPlainText()
+ is RichText.Diff -> text.toEditorMarkdownPlainText()
+ is RichText.Anchor -> ""
+ is RichText.AnchorLink -> text.toEditorMarkdownPlainText()
+ is RichText.Texts -> texts.joinToString("") { it.toEditorMarkdownPlainText() }
+ }
+}
+
+private fun escapeMarkdownText(value: String): String {
+ if (value.isBlank()) return value
+ return buildString(value.length) {
+ value.forEach { char ->
+ when (char) {
+ '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '#', '+', '!', '|', '>' -> {
+ append('\\')
+ append(char)
+ }
+
+ else -> append(char)
+ }
+ }
+ }
+}
+
+private fun formatMarkdownLink(label: String, url: String): String {
+ return if (label.isBlank()) {
+ url
+ } else {
+ "[${label}]($url)"
+ }
+}
diff --git a/domain/src/main/java/org/monogram/domain/models/webapp/RichMessagePlainText.kt b/domain/src/main/java/org/monogram/domain/models/webapp/RichMessagePlainText.kt
new file mode 100644
index 000000000..9b3a7f94d
--- /dev/null
+++ b/domain/src/main/java/org/monogram/domain/models/webapp/RichMessagePlainText.kt
@@ -0,0 +1,144 @@
+package org.monogram.domain.models.webapp
+
+fun List.toEditorPlainText(): String {
+ return joinToString("\n\n") { it.toEditorPlainText() }.trim()
+}
+
+private fun PageBlock.toEditorPlainText(): String {
+ return when (this) {
+ is PageBlock.Title -> title.toEditorPlainText()
+ is PageBlock.Subtitle -> subtitle.toEditorPlainText()
+ is PageBlock.AuthorDate -> buildString {
+ append(author.toEditorPlainText())
+ if (publishDate > 0) {
+ if (isNotBlank()) append(" • ")
+ append(publishDate)
+ }
+ }
+
+ is PageBlock.Header -> header.toEditorPlainText()
+ is PageBlock.Subheader -> subheader.toEditorPlainText()
+ is PageBlock.SectionHeading -> text.toEditorPlainText()
+ is PageBlock.Kicker -> kicker.toEditorPlainText()
+ is PageBlock.Paragraph -> text.toEditorPlainText()
+ is PageBlock.Preformatted -> text.toEditorPlainText()
+ is PageBlock.Footer -> footer.toEditorPlainText()
+ is PageBlock.Thinking -> text.toEditorPlainText()
+ is PageBlock.Divider -> "---"
+ is PageBlock.MathematicalExpression -> expression
+ is PageBlock.Anchor -> ""
+ is PageBlock.ListBlock -> items.joinToString("\n") { item ->
+ val text = item.pageBlocks.joinToString(" ") { it.toEditorPlainText() }.trim()
+ if (text.isBlank()) item.label else listOf(item.label, text).filter { it.isNotBlank() }
+ .joinToString(" ")
+ }
+
+ is PageBlock.BlockQuote -> listOf(
+ pageBlocks.toEditorPlainText(),
+ credit.toEditorPlainText()
+ )
+ .filter { it.isNotBlank() }
+ .joinToString("\n")
+
+ is PageBlock.PullQuote -> listOf(text.toEditorPlainText(), credit.toEditorPlainText())
+ .filter { it.isNotBlank() }
+ .joinToString("\n")
+
+ is PageBlock.AnimationBlock -> caption.toEditorPlainText()
+ is PageBlock.AudioBlock -> caption.toEditorPlainText()
+ is PageBlock.VoiceNoteBlock -> caption.toEditorPlainText()
+ is PageBlock.PhotoBlock -> caption.toEditorPlainText()
+ is PageBlock.VideoBlock -> caption.toEditorPlainText()
+ is PageBlock.Cover -> cover.toEditorPlainText()
+ is PageBlock.Embedded -> listOf(caption.toEditorPlainText(), url)
+ .filter { it.isNotBlank() }
+ .joinToString("\n")
+
+ is PageBlock.EmbeddedPost -> listOf(
+ author,
+ pageBlocks.toEditorPlainText(),
+ caption.toEditorPlainText()
+ )
+ .filter { it.isNotBlank() }
+ .joinToString("\n\n")
+
+ is PageBlock.Collage -> listOf(pageBlocks.toEditorPlainText(), caption.toEditorPlainText())
+ .filter { it.isNotBlank() }
+ .joinToString("\n\n")
+
+ is PageBlock.Slideshow -> listOf(
+ pageBlocks.toEditorPlainText(),
+ caption.toEditorPlainText()
+ )
+ .filter { it.isNotBlank() }
+ .joinToString("\n\n")
+
+ is PageBlock.ChatLink -> listOf(title, "@$username")
+ .filter { it.isNotBlank() }
+ .joinToString(" ")
+
+ is PageBlock.Table -> buildString {
+ val captionText = caption.toEditorPlainText()
+ if (captionText.isNotBlank()) appendLine(captionText)
+ cells.forEach { row ->
+ appendLine(row.joinToString(" | ") { it.text.toEditorPlainText() })
+ }
+ }.trim()
+
+ is PageBlock.Details -> listOf(header.toEditorPlainText(), pageBlocks.toEditorPlainText())
+ .filter { it.isNotBlank() }
+ .joinToString("\n\n")
+
+ is PageBlock.RelatedArticles -> buildString {
+ val headerText = header.toEditorPlainText()
+ if (headerText.isNotBlank()) appendLine(headerText)
+ articles.forEach { article ->
+ appendLine(listOf(article.title, article.description).filter { it.isNotBlank() }
+ .joinToString(" - "))
+ }
+ }.trim()
+
+ is PageBlock.MapBlock -> caption.toEditorPlainText()
+ is PageBlock.Unsupported -> ""
+ }
+}
+
+private fun PageBlockCaption.toEditorPlainText(): String {
+ return listOf(text.toEditorPlainText(), credit.toEditorPlainText())
+ .filter { it.isNotBlank() }
+ .joinToString("\n")
+}
+
+private fun RichText.toEditorPlainText(): String {
+ return when (this) {
+ is RichText.Plain -> text
+ is RichText.Bold -> text.toEditorPlainText()
+ is RichText.Italic -> text.toEditorPlainText()
+ is RichText.Underline -> text.toEditorPlainText()
+ is RichText.Strikethrough -> text.toEditorPlainText()
+ is RichText.Spoiler -> text.toEditorPlainText()
+ is RichText.DateTime -> text.toEditorPlainText()
+ is RichText.Mention -> text.toEditorPlainText()
+ is RichText.Hashtag -> text.toEditorPlainText()
+ is RichText.Cashtag -> text.toEditorPlainText()
+ is RichText.BotCommand -> text.toEditorPlainText()
+ is RichText.Fixed -> text.toEditorPlainText()
+ is RichText.MentionName -> text.toEditorPlainText()
+ is RichText.Url -> text.toEditorPlainText()
+ is RichText.EmailAddress -> text.toEditorPlainText()
+ is RichText.BankCardNumber -> text.toEditorPlainText()
+ is RichText.Subscript -> text.toEditorPlainText()
+ is RichText.Superscript -> text.toEditorPlainText()
+ is RichText.Marked -> text.toEditorPlainText()
+ is RichText.PhoneNumber -> text.toEditorPlainText()
+ is RichText.CustomEmoji -> alternativeText
+ is RichText.Icon -> ""
+ is RichText.MathematicalExpression -> expression
+ is RichText.Reference -> text.toEditorPlainText()
+ is RichText.ReferenceLink -> text.toEditorPlainText()
+ is RichText.Diff -> text.toEditorPlainText()
+ is RichText.Anchor -> ""
+ is RichText.AnchorLink -> text.toEditorPlainText()
+ is RichText.Texts -> texts.joinToString("") { it.toEditorPlainText() }
+ }
+}
diff --git a/domain/src/main/java/org/monogram/domain/repository/MessageAiRepository.kt b/domain/src/main/java/org/monogram/domain/repository/MessageAiRepository.kt
index 149fafe2b..1206d35cd 100644
--- a/domain/src/main/java/org/monogram/domain/repository/MessageAiRepository.kt
+++ b/domain/src/main/java/org/monogram/domain/repository/MessageAiRepository.kt
@@ -5,6 +5,7 @@ import org.monogram.domain.models.MessageEntity
interface MessageAiRepository {
val textCompositionStyles: StateFlow>
+ val supportsPromptBasedAi: Boolean
suspend fun summarizeMessage(chatId: Long, messageId: Long, toLanguageCode: String = ""): String?
@@ -19,4 +20,10 @@ interface MessageAiRepository {
): FormattedTextResult?
suspend fun fixTextWithAi(text: String, entities: List): FixedTextResult?
-}
\ No newline at end of file
+
+ suspend fun generateTextWithAi(
+ prompt: String,
+ languageCode: String = "",
+ addEmojis: Boolean = false
+ ): FormattedTextResult?
+}
diff --git a/domain/src/main/java/org/monogram/domain/repository/MessageRepository.kt b/domain/src/main/java/org/monogram/domain/repository/MessageRepository.kt
index 7972bfb00..2c89f9f24 100644
--- a/domain/src/main/java/org/monogram/domain/repository/MessageRepository.kt
+++ b/domain/src/main/java/org/monogram/domain/repository/MessageRepository.kt
@@ -182,7 +182,8 @@ interface MessageRepository :
threadId: Long? = null,
sendOptions: MessageSendOptions = MessageSendOptions(),
isRtl: Boolean? = null,
- detectAutomaticBlocks: Boolean = true
+ detectAutomaticBlocks: Boolean = true,
+ parseMode: RichTextParseMode = RichTextParseMode.Markdown
)
suspend fun sendSticker(chatId: Long, stickerPath: String, replyToMsgId: Long? = null, threadId: Long? = null)
@@ -326,7 +327,8 @@ interface MessageRepository :
messageId: Long,
markdown: String,
isRtl: Boolean? = null,
- detectAutomaticBlocks: Boolean = true
+ detectAutomaticBlocks: Boolean = true,
+ parseMode: RichTextParseMode = RichTextParseMode.Markdown
)
suspend fun editMessageCaption(chatId: Long, messageId: Long, newCaption: String, entities: List = emptyList())
suspend fun editChecklistMessage(chatId: Long, messageId: Long, checklistDraft: ChecklistDraft)
diff --git a/domain/src/main/java/org/monogram/domain/repository/RichTextParsingRepository.kt b/domain/src/main/java/org/monogram/domain/repository/RichTextParsingRepository.kt
new file mode 100644
index 000000000..b7d0db3b9
--- /dev/null
+++ b/domain/src/main/java/org/monogram/domain/repository/RichTextParsingRepository.kt
@@ -0,0 +1,13 @@
+package org.monogram.domain.repository
+
+interface RichTextParsingRepository {
+ suspend fun parseTextEntities(
+ text: String,
+ mode: RichTextParseMode
+ ): FormattedTextResult
+}
+
+enum class RichTextParseMode {
+ Markdown,
+ Html
+}
diff --git a/presentation/src/main/java/org/monogram/presentation/core/ui/SettingsTextField.kt b/presentation/src/main/java/org/monogram/presentation/core/ui/SettingsTextField.kt
index 95577959d..ef224631c 100644
--- a/presentation/src/main/java/org/monogram/presentation/core/ui/SettingsTextField.kt
+++ b/presentation/src/main/java/org/monogram/presentation/core/ui/SettingsTextField.kt
@@ -3,7 +3,6 @@ package org.monogram.presentation.core.ui
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
@@ -19,7 +18,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
-import org.monogram.presentation.core.ui.ItemPosition
@Composable
fun SettingsTextField(
@@ -29,6 +27,7 @@ fun SettingsTextField(
icon: ImageVector,
position: ItemPosition,
modifier: Modifier = Modifier,
+ containerColor: Color = MaterialTheme.colorScheme.surfaceContainer,
enabled: Boolean = true,
singleLine: Boolean = false,
minLines: Int = 1,
@@ -59,7 +58,7 @@ fun SettingsTextField(
}
Surface(
- color = MaterialTheme.colorScheme.surfaceContainer,
+ color = containerColor,
shape = shape,
modifier = modifier.fillMaxWidth()
) {
diff --git a/presentation/src/main/java/org/monogram/presentation/core/util/StringUtil.kt b/presentation/src/main/java/org/monogram/presentation/core/util/StringUtil.kt
index a1e2ab248..7dbda941d 100644
--- a/presentation/src/main/java/org/monogram/presentation/core/util/StringUtil.kt
+++ b/presentation/src/main/java/org/monogram/presentation/core/util/StringUtil.kt
@@ -14,12 +14,17 @@ import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle
-import org.monogram.core.telegram.TelegramLinkDomains
import org.koin.compose.koinInject
-import org.monogram.domain.models.*
+import org.monogram.core.telegram.TelegramLinkDomains
+import org.monogram.domain.models.MessageEntityType
+import org.monogram.domain.models.RichText
+import org.monogram.domain.models.UserModel
+import org.monogram.domain.models.UserStatusType
+import org.monogram.domain.models.UserTypeEnum
import org.monogram.presentation.R
import java.text.SimpleDateFormat
-import java.util.*
+import java.util.Date
+import java.util.Locale
fun formatLastSeen(lastSeen: Long?, context: Context, timeFormat: String): String {
if (lastSeen == null || lastSeen <= 0L) return context.getString(R.string.last_seen_recently)
@@ -109,6 +114,7 @@ fun buildRichText(
is MessageEntityType.Url -> SpanStyle(color = linkColor, textDecoration = TextDecoration.Underline)
is MessageEntityType.Mention -> SpanStyle(color = linkColor)
is MessageEntityType.Hashtag -> SpanStyle(color = linkColor)
+ is MessageEntityType.Cashtag -> SpanStyle(color = linkColor)
is MessageEntityType.BotCommand -> SpanStyle(color = linkColor)
is MessageEntityType.Email -> SpanStyle(color = linkColor, textDecoration = TextDecoration.Underline)
is MessageEntityType.PhoneNumber -> SpanStyle(
@@ -120,6 +126,15 @@ fun buildRichText(
color = linkColor,
textDecoration = TextDecoration.Underline
)
+ is MessageEntityType.DateTime -> SpanStyle(
+ color = linkColor,
+ textDecoration = TextDecoration.Underline
+ )
+
+ is MessageEntityType.MediaTimestamp -> SpanStyle(
+ color = linkColor,
+ textDecoration = TextDecoration.Underline
+ )
is MessageEntityType.TextMention -> SpanStyle(color = linkColor)
is MessageEntityType.CustomEmoji -> null
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ChatComponent.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ChatComponent.kt
index da5db91df..d9c7a1d77 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ChatComponent.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ChatComponent.kt
@@ -27,6 +27,7 @@ import org.monogram.domain.models.WebPage
import org.monogram.domain.repository.ChecklistDraft
import org.monogram.domain.repository.InlineBotResultsModel
import org.monogram.domain.repository.MessageRepository
+import org.monogram.domain.repository.RichTextParseMode
import org.monogram.domain.repository.StickerRepository
import org.monogram.presentation.core.ui.ScreenSwipeBackState
import org.monogram.presentation.core.util.AppPreferences
@@ -50,7 +51,8 @@ interface ChatComponent {
fun onSendMessage(
text: String,
entities: List = emptyList(),
- sendOptions: MessageSendOptions = MessageSendOptions()
+ sendOptions: MessageSendOptions = MessageSendOptions(),
+ parseMode: RichTextParseMode? = null
)
fun onSendSticker(stickerPath: String)
@@ -113,7 +115,11 @@ interface ChatComponent {
fun onDeleteMessage(message: MessageModel, revoke: Boolean = false)
fun onEditMessage(message: MessageModel)
fun onCancelEdit()
- fun onSaveEditedMessage(text: String, entities: List = emptyList())
+ fun onSaveEditedMessage(
+ text: String,
+ entities: List = emptyList(),
+ parseMode: RichTextParseMode? = null
+ )
fun onOpenChecklistEditor(message: MessageModel? = null)
fun onSaveChecklistDraft(draft: ChecklistDraft)
fun onToggleChecklistTask(messageId: Long, taskId: Int, isDone: Boolean)
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ChatStore.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ChatStore.kt
index a541c4b9a..0a900ae96 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ChatStore.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ChatStore.kt
@@ -13,6 +13,7 @@ import org.monogram.domain.models.MessageSendOptions
import org.monogram.domain.models.PollDraft
import org.monogram.domain.models.UserModel
import org.monogram.domain.repository.ChecklistDraft
+import org.monogram.domain.repository.RichTextParseMode
import java.io.File
interface ChatStore : Store {
@@ -21,7 +22,8 @@ interface ChatStore : Store = emptyList(),
- val sendOptions: MessageSendOptions = MessageSendOptions()
+ val sendOptions: MessageSendOptions = MessageSendOptions(),
+ val parseMode: RichTextParseMode? = null
) : Intent()
data class SendSticker(val stickerPath: String) : Intent()
@@ -84,7 +86,11 @@ interface ChatStore : Store = emptyList()) : Intent()
+ data class SaveEditedMessage(
+ val text: String,
+ val entities: List = emptyList(),
+ val parseMode: RichTextParseMode? = null
+ ) : Intent()
data class OpenChecklistEditor(val message: MessageModel? = null) : Intent()
data class SaveChecklistDraft(val draft: ChecklistDraft) : Intent()
data class ToggleChecklistTask(val messageId: Long, val taskId: Int, val isDone: Boolean) :
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ChatStoreFactory.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ChatStoreFactory.kt
index 9d0ba00d8..ddc15c0a7 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ChatStoreFactory.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ChatStoreFactory.kt
@@ -112,7 +112,12 @@ class ChatStoreFactory(
CoroutineExecutor() {
override fun executeIntent(intent: Intent) {
when (intent) {
- is Intent.SendMessage -> component.handleSendMessage(intent.text, intent.entities, intent.sendOptions)
+ is Intent.SendMessage -> component.handleSendMessage(
+ intent.text,
+ intent.entities,
+ intent.sendOptions,
+ intent.parseMode
+ )
is Intent.SendSticker -> component.handleSendSticker(intent.stickerPath)
is Intent.SendPhoto -> component.handleSendPhoto(
photoPath = intent.photoPath,
@@ -202,7 +207,11 @@ class ChatStoreFactory(
}
is Intent.CancelEdit -> component._state.update { it.copy(editingMessage = null) }
- is Intent.SaveEditedMessage -> component.handleSaveEditedMessage(intent.text, intent.entities)
+ is Intent.SaveEditedMessage -> component.handleSaveEditedMessage(
+ intent.text,
+ intent.entities,
+ intent.parseMode
+ )
is Intent.OpenChecklistEditor -> component._state.update {
it.copy(
checklistMessage = intent.message,
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/DefaultChatComponent.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/DefaultChatComponent.kt
index 59f9fa377..0ff15173d 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/DefaultChatComponent.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/DefaultChatComponent.kt
@@ -58,6 +58,7 @@ import org.monogram.domain.repository.MessageRepository
import org.monogram.domain.repository.PaymentRepository
import org.monogram.domain.repository.PinnedMessageVisibilityRepository
import org.monogram.domain.repository.PrivacyRepository
+import org.monogram.domain.repository.RichTextParseMode
import org.monogram.domain.repository.StickerRepository
import org.monogram.domain.repository.TelegramLinkRepository
import org.monogram.domain.repository.UserRepository
@@ -680,8 +681,9 @@ class DefaultChatComponent(
override fun onSendMessage(
text: String,
entities: List,
- sendOptions: MessageSendOptions
- ) = store.accept(ChatStore.Intent.SendMessage(text, entities, sendOptions))
+ sendOptions: MessageSendOptions,
+ parseMode: RichTextParseMode?
+ ) = store.accept(ChatStore.Intent.SendMessage(text, entities, sendOptions, parseMode))
override fun onSendSticker(stickerPath: String) = store.accept(ChatStore.Intent.SendSticker(stickerPath))
override fun onSendPhoto(
@@ -772,8 +774,11 @@ class DefaultChatComponent(
override fun onEditMessage(message: MessageModel) = store.accept(ChatStore.Intent.EditMessage(message))
- override fun onSaveEditedMessage(text: String, entities: List) =
- store.accept(ChatStore.Intent.SaveEditedMessage(text, entities))
+ override fun onSaveEditedMessage(
+ text: String,
+ entities: List,
+ parseMode: RichTextParseMode?
+ ) = store.accept(ChatStore.Intent.SaveEditedMessage(text, entities, parseMode))
override fun onOpenChecklistEditor(message: MessageModel?) =
run {
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/message-actions/MessageActions.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/message-actions/MessageActions.kt
index c61adb33a..de0a17e6e 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/message-actions/MessageActions.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/message-actions/MessageActions.kt
@@ -19,6 +19,7 @@ import org.monogram.domain.models.MessageEntity
import org.monogram.domain.models.MessageModel
import org.monogram.domain.models.MessageSendOptions
import org.monogram.domain.models.PollDraft
+import org.monogram.domain.repository.RichTextParseMode
import org.monogram.presentation.features.chats.common.ChatActionType
import org.monogram.presentation.features.chats.conversation.DefaultChatComponent
import org.monogram.presentation.features.chats.conversation.editor.video.VideoQuality
@@ -154,14 +155,35 @@ private inline fun DefaultChatComponent.launchPendingAttachmentSend(
internal fun DefaultChatComponent.handleSendMessage(
text: String,
entities: List,
- sendOptions: MessageSendOptions = MessageSendOptions()
+ sendOptions: MessageSendOptions = MessageSendOptions(),
+ parseMode: RichTextParseMode? = null
) {
scope.launch {
val currentState = _state.value
val replyId = currentState.replyMessage?.id
val threadId = currentState.effectiveThreadId()
val targetChatId = currentState.effectiveThreadChatId(chatId)
- repositoryMessage.sendMessage(targetChatId, text, replyId, entities, threadId, sendOptions)
+ if (parseMode == null) {
+ repositoryMessage.sendMessage(
+ targetChatId,
+ text,
+ replyId,
+ entities,
+ threadId,
+ sendOptions
+ )
+ } else {
+ repositoryMessage.sendRichMessage(
+ chatId = targetChatId,
+ markdown = text,
+ replyToMsgId = replyId,
+ threadId = threadId,
+ sendOptions = sendOptions,
+ isRtl = null,
+ detectAutomaticBlocks = true,
+ parseMode = parseMode
+ )
+ }
onCancelReply()
if (sendOptions.scheduleDate == null) {
clearDraftLinkPreviewAfterSend()
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/message-actions/MessageOperations.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/message-actions/MessageOperations.kt
index 09cf22308..6d56f8204 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/message-actions/MessageOperations.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/message-actions/MessageOperations.kt
@@ -11,6 +11,7 @@ import org.monogram.domain.models.MessageModel
import org.monogram.domain.models.MessageReactionModel
import org.monogram.domain.models.UserModel
import org.monogram.domain.repository.ChecklistDraft
+import org.monogram.domain.repository.RichTextParseMode
import org.monogram.presentation.R
import org.monogram.presentation.features.chats.conversation.ChatComponent
import org.monogram.presentation.features.chats.conversation.DefaultChatComponent
@@ -70,10 +71,21 @@ internal fun DefaultChatComponent.handleDeleteMessage(message: MessageModel, rev
}
}
-internal fun DefaultChatComponent.handleSaveEditedMessage(text: String, entities: List) {
+internal fun DefaultChatComponent.handleSaveEditedMessage(
+ text: String,
+ entities: List,
+ parseMode: RichTextParseMode?
+) {
val editingMsg = _state.value.editingMessage ?: return
scope.launch {
when (editingMsg.content) {
+ is MessageContent.RichMessage -> repositoryMessage.editRichMessage(
+ chatId = chatId,
+ messageId = editingMsg.id,
+ markdown = text,
+ parseMode = parseMode ?: RichTextParseMode.Markdown
+ )
+
is MessageContent.Photo,
is MessageContent.Video,
is MessageContent.Document,
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/ChatInputBar.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/ChatInputBar.kt
index 706897d4d..738cb5dd6 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/ChatInputBar.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/ChatInputBar.kt
@@ -47,8 +47,10 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import kotlinx.coroutines.delay
+import org.monogram.domain.models.MessageContent
import org.monogram.domain.models.MessageSendOptions
import org.monogram.domain.models.StickerModel
+import org.monogram.domain.repository.RichTextParseMode
import org.monogram.domain.repository.StickerRepository
import org.monogram.presentation.core.util.AppPreferences
import org.monogram.presentation.features.camera.CameraScreen
@@ -61,6 +63,7 @@ import org.monogram.presentation.features.chats.conversation.ui.inputbar.Compose
import org.monogram.presentation.features.chats.conversation.ui.inputbar.ComposerBotState
import org.monogram.presentation.features.chats.conversation.ui.inputbar.ComposerRowState
import org.monogram.presentation.features.chats.conversation.ui.inputbar.ComposerSuggestionState
+import org.monogram.presentation.features.chats.conversation.ui.inputbar.EditorParseMode
import org.monogram.presentation.features.chats.conversation.ui.inputbar.FullScreenEditorSheet
import org.monogram.presentation.features.chats.conversation.ui.inputbar.InputBarMode
import org.monogram.presentation.features.chats.conversation.ui.inputbar.RestrictedInputBar
@@ -70,6 +73,7 @@ import org.monogram.presentation.features.chats.conversation.ui.inputbar.Schedul
import org.monogram.presentation.features.chats.conversation.ui.inputbar.SlowModeInputBar
import org.monogram.presentation.features.chats.conversation.ui.inputbar.applyMentionSuggestion
import org.monogram.presentation.features.chats.conversation.ui.inputbar.buildEditingMessageTextValue
+import org.monogram.presentation.features.chats.conversation.ui.inputbar.buildFullScreenEditorTextValue
import org.monogram.presentation.features.chats.conversation.ui.inputbar.buildScheduledDateEpochSeconds
import org.monogram.presentation.features.chats.conversation.ui.inputbar.copyUriToPendingAttachment
import org.monogram.presentation.features.chats.conversation.ui.inputbar.copyUriToTempDocumentPath
@@ -79,6 +83,7 @@ import org.monogram.presentation.features.chats.conversation.ui.inputbar.hasAllP
import org.monogram.presentation.features.chats.conversation.ui.inputbar.isInlineBotPrefillText
import org.monogram.presentation.features.chats.conversation.ui.inputbar.parseInlineQueryInput
import org.monogram.presentation.features.chats.conversation.ui.inputbar.rememberVoiceRecorder
+import org.monogram.presentation.features.chats.conversation.ui.inputbar.toRichTextParseMode
import org.monogram.presentation.features.chats.conversation.ui.message.getEmojiFontFamily
import org.monogram.presentation.features.gallery.GalleryScreen
import org.monogram.presentation.features.gallery.components.PollComposerSheet
@@ -221,6 +226,9 @@ internal fun ChatInputBar(
var showCamera by remember { mutableStateOf(false) }
var showPollComposer by rememberSaveable { mutableStateOf(false) }
var showFullScreenEditor by rememberSaveable { mutableStateOf(false) }
+ var fullScreenEditorTextValue by rememberSaveable(stateSaver = TextFieldValue.Saver) {
+ mutableStateOf(textValue)
+ }
var showSendOptionsSheet by rememberSaveable { mutableStateOf(false) }
var attachmentPickerMode by remember { mutableStateOf(AttachmentPickerMode.Default) }
var showScheduleDatePicker by rememberSaveable { mutableStateOf(false) }
@@ -379,13 +387,18 @@ internal fun ChatInputBar(
}
}
- val sendWithOptions: (MessageSendOptions) -> Unit = sendWithOptions@{
- if (isOverMessageLimit) return@sendWithOptions
- val isTextEmpty = textValue.text.isBlank()
- val captionEntities = extractEntities(textValue.annotatedString, knownCustomEmojis)
- val isScheduling = it.scheduleDate != null
+ fun sendWithOptions(
+ options: MessageSendOptions,
+ value: TextFieldValue = textValue,
+ richTextParseMode: RichTextParseMode? = null
+ ) {
+ val isValueOverMessageLimit = value.text.length > maxMessageLength
+ if (isValueOverMessageLimit) return
+ val isTextEmpty = value.text.isBlank()
+ val captionEntities = extractEntities(value.annotatedString, knownCustomEmojis)
+ val isScheduling = options.scheduleDate != null
var sentInstantMessage = false
- val effectiveSendOptions = it.copy(
+ val effectiveSendOptions = options.copy(
disableLinkPreview = state.isDraftLinkPreviewDisabledForSend,
linkPreviewUrl = state.resolvedDraftLinkPreviewUrl ?: state.selectedDraftLinkPreviewUrl
)
@@ -404,7 +417,7 @@ internal fun ChatInputBar(
if (currentPendingAttachments.isNotEmpty() && canSendPendingAttachments) {
actions.onSendAttachments(
currentPendingAttachments,
- textValue.text,
+ value.text,
captionEntities,
effectiveSendOptions
)
@@ -413,10 +426,11 @@ internal fun ChatInputBar(
sentInstantMessage = !isScheduling
} else if (state.editingMessage != null && canWriteText) {
if (!isTextEmpty) {
- actions.onSaveEdit(textValue.text, captionEntities)
+ textValue = value
+ actions.onSaveEdit(value.text, captionEntities, richTextParseMode)
}
} else if (canWriteText && !isTextEmpty) {
- actions.onSend(textValue.text, captionEntities, effectiveSendOptions)
+ actions.onSend(value.text, captionEntities, effectiveSendOptions, richTextParseMode)
textValue = TextFieldValue("")
knownCustomEmojis.clear()
sentInstantMessage = !isScheduling
@@ -503,6 +517,19 @@ internal fun ChatInputBar(
val currentOnDraftChange by rememberUpdatedState(actions.onDraftChange)
val currentOnTyping by rememberUpdatedState(actions.onTyping)
+ val openFullScreenEditor = {
+ val editingMessage = state.editingMessage
+ fullScreenEditorTextValue = if (
+ editingMessage != null &&
+ editingMessage.content is MessageContent.RichMessage
+ ) {
+ buildFullScreenEditorTextValue(editingMessage, knownCustomEmojis) ?: textValue
+ } else {
+ textValue
+ }
+ showFullScreenEditor = true
+ }
+
LaunchedEffect(textValue.text) {
if (textValue.text != state.draftText || state.editingMessage != null) {
currentOnDraftChange(textValue.text)
@@ -516,14 +543,34 @@ internal fun ChatInputBar(
val editingMessage = state.editingMessage
if (editingMessage != null) {
if (editingMessage.id != lastEditingMessageId) {
- buildEditingMessageTextValue(editingMessage, knownCustomEmojis)?.let { newValue ->
- textValue = newValue
- focusRequester.requestFocus()
+ when (editingMessage.content) {
+ is MessageContent.RichMessage -> {
+ buildFullScreenEditorTextValue(
+ editingMessage,
+ knownCustomEmojis
+ )?.let { newValue ->
+ fullScreenEditorTextValue = newValue
+ textValue = newValue
+ showFullScreenEditor = true
+ }
+ }
+
+ else -> {
+ buildEditingMessageTextValue(
+ editingMessage,
+ knownCustomEmojis
+ )?.let { newValue ->
+ textValue = newValue
+ fullScreenEditorTextValue = newValue
+ focusRequester.requestFocus()
+ }
+ }
}
lastEditingMessageId = editingMessage.id
}
} else if (lastEditingMessageId != null) {
textValue = TextFieldValue(state.draftText, TextRange(state.draftText.length))
+ fullScreenEditorTextValue = textValue
lastEditingMessageId = null
knownCustomEmojis.clear()
}
@@ -569,6 +616,7 @@ internal fun ChatInputBar(
} else if (showScheduledMessagesSheet) {
showScheduledMessagesSheet = false
} else if (showFullScreenEditor) {
+ textValue = fullScreenEditorTextValue
showFullScreenEditor = false
} else if (state.pendingMediaPaths.isNotEmpty()) {
actions.onCancelMedia()
@@ -846,7 +894,7 @@ internal fun ChatInputBar(
},
onCommandClick = { command ->
if (isSlowModeActive || !canWriteText) return@ChatInputBarComposerSection
- actions.onSend("/$command", emptyList(), MessageSendOptions())
+ actions.onSend("/$command", emptyList(), MessageSendOptions(), null)
textValue = TextFieldValue("")
activateSlowModeCooldown()
},
@@ -897,12 +945,12 @@ internal fun ChatInputBar(
}
isStickerMenuVisible = false
},
- onOpenFullScreenEditor = { showFullScreenEditor = true },
+ onOpenFullScreenEditor = openFullScreenEditor,
onOpenScheduledMessages = {
actions.onRefreshScheduledMessages()
showScheduledMessagesSheet = true
},
- onSendWithOptions = sendWithOptions,
+ onSendWithOptions = { options -> sendWithOptions(options) },
onShowSendOptionsMenu = {
openStickerMenuAfterKeyboardClosed = false
openKeyboardAfterStickerMenuClosed = false
@@ -983,22 +1031,34 @@ internal fun ChatInputBar(
FullScreenEditorSheet(
visible = showFullScreenEditor,
- textValue = textValue,
- onTextValueChange = { textValue = it },
+ textValue = fullScreenEditorTextValue,
+ onTextValueChange = { fullScreenEditorTextValue = it },
canWriteText = canWriteText,
pendingMediaPaths = if (state.pendingMediaPaths.isNotEmpty()) state.pendingMediaPaths else state.pendingDocumentPaths,
knownCustomEmojis = knownCustomEmojis,
emojiFontFamily = emojiFontFamily,
isKeyboardVisible = isKeyboardVisible,
- isOverMessageLimit = isOverMessageLimit,
- currentMessageLength = currentMessageLength,
maxMessageLength = maxMessageLength,
+ initialParseMode = if (state.editingMessage?.content is MessageContent.RichMessage) {
+ EditorParseMode.Markdown
+ } else {
+ EditorParseMode.Plain
+ },
+ sendAsRichMessage = state.editingMessage?.content is MessageContent.RichMessage,
stickerRepository = stickerRepository,
isPremiumUser = state.isPremiumUser,
isSecretChat = state.isSecretChat,
- onDismiss = { showFullScreenEditor = false },
- onSend = {
- sendWithOptions(MessageSendOptions())
+ onDismiss = {
+ textValue = fullScreenEditorTextValue
+ showFullScreenEditor = false
+ },
+ onSend = { outgoingValue, parseMode ->
+ sendWithOptions(
+ MessageSendOptions(),
+ outgoingValue,
+ parseMode.toRichTextParseMode()
+ )
+ fullScreenEditorTextValue = TextFieldValue("")
showFullScreenEditor = false
},
onEditorFocus = { isStickerMenuVisible = false },
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/MessageBubbleContainer.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/MessageBubbleContainer.kt
index f188ba8cc..6ed679dd1 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/MessageBubbleContainer.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/MessageBubbleContainer.kt
@@ -7,7 +7,6 @@ import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@@ -29,6 +28,7 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
@@ -100,16 +100,26 @@ internal fun MessageBubbleContainer(
val configuration = LocalConfiguration.current
val screenWidth = configuration.screenWidthDp.dp
val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
+ val isOutgoing = msg.isOutgoing
+ val contentPrefersExpandedWidth = remember(msg.content) {
+ msg.content.prefersExpandedBubbleWidth()
+ }
- val maxWidth = remember(isLandscape, screenWidth) {
- if (isLandscape) {
- (screenWidth * 0.6f).coerceAtMost(450.dp)
- } else {
- (screenWidth * 0.85f).coerceAtMost(360.dp)
- }
+ val maxWidth = remember(
+ isLandscape,
+ screenWidth,
+ contentPrefersExpandedWidth,
+ behavior.isGroup,
+ isOutgoing
+ ) {
+ resolveMessageBubbleMaxWidth(
+ screenWidth = screenWidth,
+ isLandscape = isLandscape,
+ prefersExpandedWidth = contentPrefersExpandedWidth,
+ hasLeadingAvatar = behavior.isGroup && !isOutgoing
+ )
}
- val isOutgoing = msg.isOutgoing
val topSpacing = if (!senderGrouping.isSameSenderAbove) 8.dp else 2.dp
val dragOffsetX = remember { Animatable(0f) }
val coroutineScope = rememberCoroutineScope()
@@ -160,9 +170,6 @@ internal fun MessageBubbleContainer(
)
}
}
- val contentPrefersExpandedWidth = remember(msg.content) {
- msg.content.prefersExpandedBubbleWidth()
- }
MessageBubbleGestureLayer(
modifier = Modifier.padding(top = topSpacing),
@@ -223,15 +230,11 @@ internal fun MessageBubbleContainer(
}
) {
MessageBubbleContentHost(
- modifier = Modifier
- .then(
- if (contentPrefersExpandedWidth) {
- Modifier.fillMaxWidth()
- } else {
- Modifier.width(IntrinsicSize.Max)
- }
- )
- .widthIn(max = maxWidth),
+ modifier = (if (contentPrefersExpandedWidth) {
+ Modifier.fillMaxWidth()
+ } else {
+ Modifier
+ }).widthIn(max = maxWidth),
msg = msg,
newerMsg = newerMsg,
isOutgoing = isOutgoing,
@@ -511,7 +514,7 @@ private fun MessageContentSelector(
modifier = if (contentPrefersExpandedWidth) {
Modifier.fillMaxWidth()
} else {
- Modifier.width(IntrinsicSize.Max)
+ Modifier
},
horizontalAlignment = if (isOutgoing) Alignment.End else Alignment.Start
) {
@@ -555,7 +558,12 @@ private fun MessageContentSelector(
onLongClick = onBubbleLongClick,
onLinkPreviewAction = onLinkPreviewAction,
toProfile = toProfile,
- onForwardOriginClick = onForwardOriginClick
+ onForwardOriginClick = onForwardOriginClick,
+ modifier = if (contentPrefersExpandedWidth) {
+ Modifier.fillMaxWidth()
+ } else {
+ Modifier
+ }
)
}
@@ -885,3 +893,27 @@ private fun MessageReplyMarkup(
private fun androidx.compose.ui.geometry.Size.toOffset() = Offset(width, height)
+internal fun resolveMessageBubbleMaxWidth(
+ screenWidth: Dp,
+ isLandscape: Boolean,
+ prefersExpandedWidth: Boolean,
+ hasLeadingAvatar: Boolean
+): Dp {
+ val availableWidth = (screenWidth - if (hasLeadingAvatar) 48.dp else 0.dp)
+ .coerceAtLeast(0.dp)
+
+ return if (prefersExpandedWidth) {
+ if (isLandscape) {
+ (availableWidth * 0.92f).coerceAtMost(600.dp)
+ } else {
+ (availableWidth * 0.94f).coerceAtMost(500.dp)
+ }
+ } else {
+ if (isLandscape) {
+ (availableWidth * 0.6f).coerceAtMost(450.dp)
+ } else {
+ (availableWidth * 0.85f).coerceAtMost(360.dp)
+ }
+ }
+}
+
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentInputConfiguration.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentInputConfiguration.kt
index 7ae26c1f5..1f4270c41 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentInputConfiguration.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentInputConfiguration.kt
@@ -132,8 +132,8 @@ internal fun rememberChatInputBarActions(
onDraftLinkPreviewAction
) {
ChatInputBarActions(
- onSend = { text, entities, options ->
- component.onSendMessage(text, entities, options)
+ onSend = { text, entities, options, parseMode ->
+ component.onSendMessage(text, entities, options, parseMode)
},
onStickerClick = component::onSendSticker,
onGifClick = component::onSendGif,
@@ -145,7 +145,9 @@ internal fun rememberChatInputBarActions(
onSendVoice = component::onSendVoice,
onCancelReply = component::onCancelReply,
onCancelEdit = component::onCancelEdit,
- onSaveEdit = component::onSaveEditedMessage,
+ onSaveEdit = { text, entities, parseMode ->
+ component.onSaveEditedMessage(text, entities, parseMode)
+ },
onDraftChange = component::onDraftChange,
onSelectDraftLinkPreview = component::onSelectDraftLinkPreview,
onDismissDraftLinkPreview = component::onDismissDraftLinkPreview,
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ChatInputBarComposerSection.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ChatInputBarComposerSection.kt
index 8458d7892..e159530c0 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ChatInputBarComposerSection.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ChatInputBarComposerSection.kt
@@ -41,6 +41,7 @@ import androidx.compose.ui.unit.dp
import org.monogram.domain.models.GifModel
import org.monogram.domain.models.KeyboardButtonModel
import org.monogram.domain.models.LinkPreviewTarget
+import org.monogram.domain.models.MessageContent
import org.monogram.domain.models.MessageModel
import org.monogram.domain.models.MessageSendOptions
import org.monogram.domain.models.ReplyMarkupModel
@@ -150,7 +151,9 @@ internal fun ChatInputBarComposerSection(
canPasteMediaFromClipboard = canPasteMediaFromClipboard,
pendingMediaPaths = attachments.pendingMediaPaths,
pendingDocumentPaths = attachments.pendingDocumentPaths,
- showExpandEditorAction = rowState.textValue.text.contains('\n') || rowState.textValue.text.length > 150
+ showExpandEditorAction = rowState.editingMessage?.content is MessageContent.RichMessage ||
+ rowState.textValue.text.contains('\n') ||
+ rowState.textValue.text.length > 150
)
}
val sendButtonState = remember(
@@ -405,7 +408,7 @@ private fun ComposerMainRow(
Row(
modifier = Modifier
.fillMaxWidth()
- .padding(horizontal = 8.dp, vertical = 8.dp),
+ .padding(horizontal = 8.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically
) {
ComposerInputSlot(
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ChatInputBarContract.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ChatInputBarContract.kt
index cadc349d8..e02ce1746 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ChatInputBarContract.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ChatInputBarContract.kt
@@ -18,6 +18,7 @@ import org.monogram.domain.models.ReplyMarkupModel
import org.monogram.domain.models.UserModel
import org.monogram.domain.models.WebPage
import org.monogram.domain.repository.InlineBotResultsModel
+import org.monogram.domain.repository.RichTextParseMode
import org.monogram.presentation.features.chats.conversation.ui.message.LinkPreviewAction
import org.monogram.presentation.features.share.PendingAttachment
@@ -62,7 +63,7 @@ data class ChatInputBarState(
@Immutable
internal data class ChatInputBarActions(
- val onSend: (String, List, MessageSendOptions) -> Unit,
+ val onSend: (String, List, MessageSendOptions, RichTextParseMode?) -> Unit,
val onStickerClick: (String) -> Unit = {},
val onGifClick: (GifModel) -> Unit = {},
val onAttachClick: () -> Unit = {},
@@ -70,7 +71,7 @@ internal data class ChatInputBarActions(
val onSendVoice: (String, Int, ByteArray) -> Unit = { _, _, _ -> },
val onCancelReply: () -> Unit = {},
val onCancelEdit: () -> Unit = {},
- val onSaveEdit: (String, List) -> Unit = { _, _ -> },
+ val onSaveEdit: (String, List, RichTextParseMode?) -> Unit = { _, _, _ -> },
val onDraftChange: (String) -> Unit = {},
val onSelectDraftLinkPreview: (String) -> Unit = {},
val onDismissDraftLinkPreview: () -> Unit = {},
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ChatInputBarHelpers.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ChatInputBarHelpers.kt
index 351fb2c29..baf18baf2 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ChatInputBarHelpers.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ChatInputBarHelpers.kt
@@ -14,6 +14,7 @@ import org.monogram.domain.models.MessageModel
import org.monogram.domain.models.StickerFormat
import org.monogram.domain.models.StickerModel
import org.monogram.domain.models.UserModel
+import org.monogram.domain.models.webapp.toEditorMarkdown
import org.monogram.presentation.core.util.copyUriToTempDocumentFile
import org.monogram.presentation.core.util.copyUriToTempMediaFile
import org.monogram.presentation.features.share.PendingAttachment
@@ -109,6 +110,25 @@ internal fun buildEditingMessageTextValue(
)
}
+internal fun buildFullScreenEditorTextValue(
+ message: MessageModel,
+ knownCustomEmojis: MutableMap
+): TextFieldValue? {
+ return when (val content = message.content) {
+ is MessageContent.RichMessage -> {
+ knownCustomEmojis.clear()
+ val markdown = content.markdownSource?.takeIf { it.isNotBlank() }
+ ?: content.blocks.toEditorMarkdown()
+ TextFieldValue(
+ text = markdown,
+ selection = TextRange(markdown.length)
+ )
+ }
+
+ else -> buildEditingMessageTextValue(message, knownCustomEmojis)
+ }
+}
+
internal fun buildTextFieldValueFromTextAndEntities(
text: String,
entities: List,
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/EditorRichTextParsing.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/EditorRichTextParsing.kt
new file mode 100644
index 000000000..276cca13f
--- /dev/null
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/EditorRichTextParsing.kt
@@ -0,0 +1,1414 @@
+package org.monogram.presentation.features.chats.conversation.ui.inputbar
+
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.TextFieldValue
+import org.monogram.domain.models.MessageEntityType
+import org.monogram.domain.repository.RichTextParseMode
+
+internal const val LATEX_TAG = "latex_expression"
+internal const val EDITOR_HEADING_TAG = "editor_heading"
+internal const val EDITOR_DIVIDER_TAG = "editor_divider"
+
+internal enum class EditorParseMode {
+ Plain,
+ Markdown,
+ Html;
+
+ fun next(): EditorParseMode {
+ return when (this) {
+ Plain -> Markdown
+ Markdown -> Html
+ Html -> Plain
+ }
+ }
+}
+
+internal fun applyEditorFormatting(value: TextFieldValue, mode: EditorParseMode): TextFieldValue {
+ val annotated = when (mode) {
+ EditorParseMode.Plain -> value.annotatedString
+ EditorParseMode.Markdown -> MarkdownRichTextParser(value.annotatedString).parse()
+ EditorParseMode.Html -> HtmlRichTextParser(value.annotatedString).parse()
+ }
+ return value.copy(annotatedString = annotated, selection = TextRange(annotated.length))
+}
+
+internal fun applyMarkdownFormatting(value: TextFieldValue): TextFieldValue {
+ return applyEditorFormatting(value, EditorParseMode.Markdown)
+}
+
+internal fun applyHtmlFormatting(value: TextFieldValue): TextFieldValue {
+ return applyEditorFormatting(value, EditorParseMode.Html)
+}
+
+internal fun EditorParseMode.toRichTextParseMode(): RichTextParseMode? {
+ return when (this) {
+ EditorParseMode.Plain -> null
+ EditorParseMode.Markdown -> RichTextParseMode.Markdown
+ EditorParseMode.Html -> RichTextParseMode.Html
+ }
+}
+
+internal fun normalizeEditorMarkupForSending(text: String, mode: EditorParseMode): String {
+ return when (mode) {
+ EditorParseMode.Plain -> text
+ EditorParseMode.Markdown -> normalizeMarkdownForSending(text)
+ EditorParseMode.Html -> normalizeHtmlForSending(text)
+ }
+}
+
+internal class MarkdownRichTextParser(private val source: AnnotatedString) {
+ private val input = source.text
+ private val builder = ParsedAnnotatedStringBuilder(source)
+ private val markerStarts = mutableMapOf>()
+ private var index = 0
+ private var lineStart = true
+ private var activeQuote: MarkdownQuote? = null
+ private var quoteLineOpen = false
+ private var activeHeading: MarkdownHeading? = null
+
+ fun parse(): AnnotatedString {
+ while (index < input.length) {
+ if (tryAppendEscapedChar()) continue
+ if (activeInlineCode() && tryToggleInlineMarker(codeOnly = true)) continue
+ if (tryAppendFenceBlock()) continue
+ if (lineStart && tryAppendQuotedTable()) continue
+ if (lineStart && tryAppendQuoteLine()) continue
+ if (lineStart && activeQuote != null && !quoteLineOpen) {
+ closeActiveQuote()
+ continue
+ }
+ if (lineStart && tryOpenHeading()) continue
+ if (lineStart && tryAppendDivider()) continue
+ if (lineStart && tryAppendTable()) continue
+ if (lineStart && tryAppendListMarker()) continue
+ if (tryAppendLink()) continue
+ if (tryAppendLatexBlock()) continue
+ if (tryAppendInlineLatex()) continue
+ if (tryToggleInlineMarker()) continue
+
+ appendInputChar(index)
+ index++
+ }
+
+ closeActiveQuote()
+ closeActiveHeading()
+ return builder.build()
+ }
+
+ private fun tryAppendEscapedChar(): Boolean {
+ if (input.getOrNull(index) != '\\' || index + 1 >= input.length) return false
+ builder.appendChar(input[index + 1], index)
+ lineStart = false
+ index += 2
+ return true
+ }
+
+ private fun activeInlineCode(): Boolean = markerStarts["`"].isNullOrEmpty().not()
+
+ private fun tryAppendFenceBlock(): Boolean {
+ if (!input.startsWith("```", index)) return false
+
+ val infoLineBreak = input.indexOf('\n', startIndex = index + 3)
+ if (infoLineBreak == -1) return false
+
+ val closingFence = input.indexOf("```", startIndex = infoLineBreak + 1)
+ if (closingFence == -1) return false
+
+ val language = input.substring(index + 3, infoLineBreak).trim()
+ val blockStart = builder.length
+ var cursor = infoLineBreak + 1
+ while (cursor < closingFence) {
+ appendInputChar(cursor)
+ cursor++
+ }
+ val blockEnd = builder.length
+ if (blockStart < blockEnd) {
+ builder.addRichEntity(blockStart, blockEnd, MessageEntityType.Pre(language))
+ }
+ index = closingFence + 3
+ lineStart = builder.lastChar == '\n'
+ return true
+ }
+
+ private fun tryOpenQuote(): Boolean {
+ val type = when {
+ input.startsWith(">>> ", index) -> MessageEntityType.BlockQuoteExpandable
+ input.startsWith("> ", index) -> MessageEntityType.BlockQuote
+ else -> null
+ } ?: return false
+
+ activeQuote = MarkdownQuote(start = builder.length, type = type)
+ index += if (type is MessageEntityType.BlockQuoteExpandable) 4 else 2
+ lineStart = false
+ return true
+ }
+
+ private fun tryAppendQuoteLine(): Boolean {
+ val (prefixLength, type) = when {
+ input.startsWith(">>> ", index) -> 4 to MessageEntityType.BlockQuoteExpandable
+ input.startsWith("> ", index) -> 2 to MessageEntityType.BlockQuote
+ input.startsWith(">", index) -> 1 to MessageEntityType.BlockQuote
+ else -> return false
+ }
+
+ if (activeQuote == null) {
+ activeQuote = MarkdownQuote(start = builder.length, type = type)
+ } else if (type is MessageEntityType.BlockQuoteExpandable && activeQuote?.type != type) {
+ activeQuote = activeQuote?.copy(type = type)
+ }
+
+ quoteLineOpen = true
+ index += prefixLength
+ lineStart = true
+ return true
+ }
+
+ private fun tryAppendQuotedTable(): Boolean {
+ val prefix = quotePrefixAt(index) ?: return false
+ val headerEnd = currentLineEnd(index)
+ if (headerEnd >= input.length) return false
+ val separatorStart = headerEnd + 1
+ val separatorEnd = currentLineEnd(separatorStart)
+
+ val headerLine = input.substring(index, headerEnd).removePrefix(prefix)
+ val separatorLine = input.substring(separatorStart, separatorEnd).removePrefix(prefix)
+ val headerCells = parseMarkdownTableCells(headerLine) ?: return false
+ val separatorCells = parseMarkdownTableSeparator(
+ line = separatorLine,
+ expectedColumns = headerCells.size
+ ) ?: return false
+ if (separatorCells.size != headerCells.size) return false
+
+ val quoteType = if (prefix.startsWith(">>>")) {
+ MessageEntityType.BlockQuoteExpandable
+ } else {
+ MessageEntityType.BlockQuote
+ }
+ if (activeQuote == null) {
+ activeQuote = MarkdownQuote(start = builder.length, type = quoteType)
+ } else if (quoteType is MessageEntityType.BlockQuoteExpandable && activeQuote?.type != quoteType) {
+ activeQuote = activeQuote?.copy(type = quoteType)
+ }
+
+ val rows = mutableListOf(headerCells)
+ var cursor = if (separatorEnd < input.length) separatorEnd + 1 else separatorEnd
+ while (cursor < input.length) {
+ val rowEnd = currentLineEnd(cursor)
+ val rowPrefix = quotePrefixAt(cursor) ?: break
+ val cells = parseMarkdownTableCells(
+ input.substring(cursor, rowEnd).removePrefix(rowPrefix)
+ ) ?: break
+ if (cells.size != headerCells.size) break
+ rows += cells
+ cursor = if (rowEnd < input.length) rowEnd + 1 else rowEnd
+ }
+
+ val formattedTable = formatMarkdownTable(rows)
+ if (formattedTable.isBlank()) return false
+
+ val start = builder.length
+ builder.appendText(formattedTable, index)
+ val end = builder.length
+ builder.addRichEntity(start, end, MessageEntityType.Pre("table"))
+
+ if (cursor < input.length && builder.lastChar != '\n') {
+ builder.appendChar('\n', cursor - 1)
+ }
+ index = cursor
+ lineStart = index >= input.length || input.getOrNull(index - 1) == '\n'
+ quoteLineOpen = false
+ return true
+ }
+
+ private fun tryOpenHeading(): Boolean {
+ var cursor = index
+ var leadingSpaces = 0
+ while (cursor < input.length && input[cursor] == ' ' && leadingSpaces < 3) {
+ cursor++
+ leadingSpaces++
+ }
+ if (cursor >= input.length || input[cursor] != '#') return false
+
+ var level = 0
+ while (cursor + level < input.length && input[cursor + level] == '#' && level < 3) {
+ level++
+ }
+ if (level == 0) return false
+ if (cursor + level < input.length && input[cursor + level] == '#') return false
+ if (cursor + level >= input.length || !input[cursor + level].isWhitespace()) return false
+
+ var contentStart = cursor + level
+ while (contentStart < input.length && input[contentStart].isWhitespace() && input[contentStart] != '\n') {
+ contentStart++
+ }
+ val lineEnd = currentLineEnd(index)
+ if (contentStart >= lineEnd) return false
+
+ activeHeading = MarkdownHeading(
+ start = builder.length,
+ level = level
+ )
+ index = contentStart
+ lineStart = false
+ return true
+ }
+
+ private fun tryAppendDivider(): Boolean {
+ val lineEnd = currentLineEnd(index)
+ val line = input.substring(index, lineEnd).trim()
+ if (line.length < 3) return false
+ val marker = line.first()
+ if (marker !in setOf('-', '_', '*')) return false
+ if (line.any { it != marker && !it.isWhitespace() }) return false
+ if (line.count { it == marker } < 3) return false
+
+ val start = builder.length
+ builder.appendText("────────────────────", index)
+ val end = builder.length
+ builder.addEditorBlock(EDITOR_DIVIDER_TAG, "", start, end)
+
+ if (lineEnd < input.length) {
+ builder.appendChar('\n', lineEnd)
+ index = lineEnd + 1
+ lineStart = true
+ } else {
+ index = lineEnd
+ lineStart = false
+ }
+ return true
+ }
+
+ private fun tryAppendTable(): Boolean {
+ val headerEnd = currentLineEnd(index)
+ if (headerEnd >= input.length) return false
+ val separatorStart = headerEnd + 1
+ val separatorEnd = currentLineEnd(separatorStart)
+
+ val headerCells =
+ parseMarkdownTableCells(input.substring(index, headerEnd).stripQuotePrefix())
+ ?: return false
+ val separatorCells = parseMarkdownTableSeparator(
+ line = input.substring(separatorStart, separatorEnd).stripQuotePrefix(),
+ expectedColumns = headerCells.size
+ ) ?: return false
+ if (separatorCells.size != headerCells.size) return false
+
+ val rows = mutableListOf(headerCells)
+ var cursor = if (separatorEnd < input.length) separatorEnd + 1 else separatorEnd
+ while (cursor < input.length) {
+ val rowEnd = currentLineEnd(cursor)
+ val cells =
+ parseMarkdownTableCells(input.substring(cursor, rowEnd).stripQuotePrefix()) ?: break
+ if (cells.size != headerCells.size) break
+ rows += cells
+ cursor = if (rowEnd < input.length) rowEnd + 1 else rowEnd
+ }
+
+ val formattedTable = formatMarkdownTable(rows)
+ if (formattedTable.isBlank()) return false
+
+ val start = builder.length
+ builder.appendText(formattedTable, index)
+ val end = builder.length
+ builder.addRichEntity(start, end, MessageEntityType.Pre("table"))
+
+ if (cursor < input.length && builder.lastChar != '\n') {
+ builder.appendChar('\n', cursor - 1)
+ }
+ index = cursor
+ lineStart = index >= input.length || input.getOrNull(index - 1) == '\n'
+ return true
+ }
+
+ private fun tryAppendListMarker(): Boolean {
+ val lineEnd = currentLineEnd(index)
+ if (lineEnd <= index) return false
+ val line = input.substring(index, lineEnd)
+
+ val unorderedMatch = MARKDOWN_UNORDERED_LIST_REGEX.find(line)
+ if (unorderedMatch != null && unorderedMatch.range.first == 0) {
+ val indent = unorderedMatch.groupValues[1]
+ val markerEnd = index + unorderedMatch.range.last + 1
+ builder.appendText(indent, index)
+ builder.appendText("\u2022 ", index)
+ index = markerEnd
+ lineStart = false
+ return true
+ }
+
+ val orderedMatch = MARKDOWN_ORDERED_LIST_REGEX.find(line)
+ if (orderedMatch != null && orderedMatch.range.first == 0) {
+ val indent = orderedMatch.groupValues[1]
+ val number = orderedMatch.groupValues[2]
+ val markerEnd = index + orderedMatch.range.last + 1
+ builder.appendText(indent, index)
+ builder.appendText("$number. ", index)
+ index = markerEnd
+ lineStart = false
+ return true
+ }
+
+ return false
+ }
+
+ private fun tryAppendLink(): Boolean {
+ val match = LINK_REGEX.find(input, index) ?: return false
+ if (match.range.first != index) return false
+
+ val label = match.groupValues[1]
+ val url = match.groupValues[2].trim()
+ if (label.isEmpty() || url.isEmpty()) return false
+
+ val start = builder.length
+ label.forEachIndexed { offset, char ->
+ builder.appendChar(char, index + 1 + offset)
+ }
+ val end = builder.length
+ builder.addRichEntity(start, end, MessageEntityType.TextUrl(url))
+ index = match.range.last + 1
+ lineStart = builder.lastChar == '\n'
+ return true
+ }
+
+ private fun tryAppendLatexBlock(): Boolean {
+ if (!input.startsWith("$$", index)) return false
+ val closing = findClosingDelimiter("$$", index + 2) ?: return false
+ val start = builder.length
+ var cursor = index + 2
+ while (cursor < closing) {
+ appendInputChar(cursor)
+ cursor++
+ }
+ val end = builder.length
+ builder.addLatex(start, end, displayMode = true)
+ index = closing + 2
+ lineStart = builder.lastChar == '\n'
+ return true
+ }
+
+ private fun tryAppendInlineLatex(): Boolean {
+ if (input.getOrNull(index) != '$' || input.getOrNull(index + 1) == '$') return false
+ val closing = findClosingDelimiter("$", index + 1) ?: return false
+ val start = builder.length
+ var cursor = index + 1
+ while (cursor < closing) {
+ appendInputChar(cursor)
+ cursor++
+ }
+ val end = builder.length
+ builder.addLatex(start, end, displayMode = false)
+ index = closing + 1
+ lineStart = builder.lastChar == '\n'
+ return true
+ }
+
+ private fun tryToggleInlineMarker(codeOnly: Boolean = false): Boolean {
+ val marker = (if (codeOnly) CODE_ONLY_MARKERS else MARKDOWN_MARKERS)
+ .firstOrNull { input.startsWith(it, index) }
+ ?: return false
+ val type = markerToType(marker) ?: return false
+ val starts = markerStarts.getOrPut(marker) { mutableListOf() }
+
+ if (starts.isNotEmpty()) {
+ val start = starts.removeAt(starts.lastIndex)
+ val end = builder.length
+ if (start < end) {
+ builder.addRichEntity(start, end, type)
+ }
+ index += marker.length
+ return true
+ }
+
+ if (findClosingDelimiter(marker, index + marker.length) == null) return false
+ starts += builder.length
+ index += marker.length
+ return true
+ }
+
+ private fun findClosingDelimiter(delimiter: String, fromIndex: Int): Int? {
+ var cursor = fromIndex
+ while (cursor <= input.length - delimiter.length) {
+ if (input[cursor] == '\\') {
+ cursor += 2
+ continue
+ }
+ if (input.startsWith(delimiter, cursor)) return cursor
+ cursor++
+ }
+ return null
+ }
+
+ private fun appendInputChar(sourceIndex: Int) {
+ val char = input[sourceIndex]
+ if (char == '\n') {
+ closeActiveHeading()
+ quoteLineOpen = false
+ }
+ builder.appendChar(char, sourceIndex)
+ lineStart = char == '\n'
+ }
+
+ private fun closeActiveQuote() {
+ val quote = activeQuote ?: return
+ val end = builder.length
+ if (quote.start < end) {
+ builder.addRichEntity(quote.start, end, quote.type)
+ }
+ activeQuote = null
+ quoteLineOpen = false
+ }
+
+ private fun closeActiveHeading() {
+ val heading = activeHeading ?: return
+ val end = builder.length
+ if (heading.start < end) {
+ builder.addEditorBlock(
+ tag = EDITOR_HEADING_TAG,
+ item = heading.level.toString(),
+ start = heading.start,
+ end = end
+ )
+ }
+ activeHeading = null
+ }
+
+ private fun currentLineEnd(fromIndex: Int): Int {
+ val newlineIndex = input.indexOf('\n', startIndex = fromIndex)
+ return if (newlineIndex == -1) input.length else newlineIndex
+ }
+
+ private fun String.stripQuotePrefix(): String {
+ return when {
+ startsWith(">>> ") -> substring(4)
+ startsWith("> ") -> substring(2)
+ startsWith(">") -> substring(1)
+ else -> this
+ }
+ }
+
+ private fun quotePrefixAt(startIndex: Int): String? {
+ return when {
+ input.startsWith(">>> ", startIndex) -> ">>> "
+ input.startsWith("> ", startIndex) -> "> "
+ input.startsWith(">", startIndex) -> ">"
+ else -> null
+ }
+ }
+}
+
+internal class HtmlRichTextParser(private val source: AnnotatedString) {
+ private val input = source.text
+ private val builder = ParsedAnnotatedStringBuilder(source)
+ private val openTags = mutableListOf()
+ private var index = 0
+
+ fun parse(): AnnotatedString {
+ while (index < input.length) {
+ if (tryAppendHtmlTable()) continue
+ if (input[index] == '<' && tryHandleTag()) continue
+ if (input[index] == '&' && tryHandleEntity()) continue
+
+ builder.appendChar(input[index], index)
+ index++
+ }
+
+ return builder.build()
+ }
+
+ private fun tryHandleTag(): Boolean {
+ if (input.startsWith("", startIndex = index + 4)
+ if (commentEnd != -1) {
+ index = commentEnd + 3
+ return true
+ }
+ }
+
+ val token = parseHtmlTagToken(input, index) ?: return false
+ val handled = when {
+ token.isClosing -> closeTag(token)
+ else -> openTag(token)
+ }
+
+ if (!handled) return false
+ index += token.raw.length
+ return true
+ }
+
+ private fun openTag(token: HtmlTagToken): Boolean {
+ return when (val name = token.name.lowercase()) {
+ "br" -> {
+ builder.appendChar('\n', index)
+ true
+ }
+
+ "b", "strong" -> pushOrApply(token, HtmlOpenKind.Rich(MessageEntityType.Bold))
+ "i", "em" -> pushOrApply(token, HtmlOpenKind.Rich(MessageEntityType.Italic))
+ "u", "ins" -> pushOrApply(token, HtmlOpenKind.Rich(MessageEntityType.Underline))
+ "s", "strike", "del" -> pushOrApply(
+ token,
+ HtmlOpenKind.Rich(MessageEntityType.Strikethrough)
+ )
+
+ "tg-spoiler" -> pushOrApply(token, HtmlOpenKind.Rich(MessageEntityType.Spoiler))
+ "span" -> {
+ if (token.attributes["class"]?.contains("tg-spoiler", ignoreCase = true) == true) {
+ pushOrApply(token, HtmlOpenKind.Rich(MessageEntityType.Spoiler))
+ } else {
+ false
+ }
+ }
+
+ "a" -> {
+ val href = token.attributes["href"].orEmpty().trim()
+ when {
+ href.startsWith("tg://user?id=") -> {
+ val userId =
+ href.substringAfter("tg://user?id=").toLongOrNull() ?: return false
+ pushOrApply(token, HtmlOpenKind.TextMention(userId))
+ }
+
+ href.isNotBlank() -> pushOrApply(
+ token,
+ HtmlOpenKind.Rich(MessageEntityType.TextUrl(href))
+ )
+
+ else -> false
+ }
+ }
+
+ "code" -> {
+ if (openTags.lastOrNull()?.kind is HtmlOpenKind.Pre) {
+ val language = token.attributes["class"]
+ ?.split(' ', '\t', '\n')
+ ?.firstOrNull { it.startsWith("language-") }
+ ?.substringAfter("language-")
+ .orEmpty()
+ if (language.isNotBlank()) {
+ val preTag = openTags.last()
+ preTag.kind = HtmlOpenKind.Pre(language)
+ }
+ false
+ } else {
+ pushOrApply(token, HtmlOpenKind.Rich(MessageEntityType.Code))
+ }
+ }
+
+ "pre" -> {
+ ensureBlockBoundary()
+ pushOrApply(
+ token,
+ HtmlOpenKind.Pre(
+ token.attributes["language"]
+ ?: token.attributes["lang"]
+ ?: token.attributes["data-language"]
+ ?: ""
+ )
+ )
+ }
+
+ "h1", "h2", "h3" -> {
+ ensureBlockBoundary()
+ pushOrApply(
+ token,
+ HtmlOpenKind.Heading(name.removePrefix("h").toIntOrNull()?.coerceIn(1, 3) ?: 1)
+ )
+ }
+
+ "blockquote" -> {
+ ensureBlockBoundary()
+ val type = if (token.attributes.containsKey("expandable")) {
+ MessageEntityType.BlockQuoteExpandable
+ } else {
+ MessageEntityType.BlockQuote
+ }
+ pushOrApply(token, HtmlOpenKind.Rich(type))
+ }
+
+ "p", "div" -> {
+ ensureBlockBoundary()
+ pushOrApply(token, HtmlOpenKind.BlockContainer)
+ }
+
+ "ul", "ol" -> {
+ ensureBlockBoundary()
+ pushOrApply(token, HtmlOpenKind.ListContainer(ordered = name == "ol"))
+ }
+
+ "li" -> {
+ ensureBlockBoundary()
+ val listContainer =
+ openTags.lastOrNull { it.kind is HtmlOpenKind.ListContainer } ?: return false
+ val listKind = listContainer.kind as HtmlOpenKind.ListContainer
+ val prefix = if (listKind.ordered) {
+ "${listKind.nextIndex}. ".also { listKind.nextIndex++ }
+ } else {
+ "\u2022 "
+ }
+ builder.appendText(prefix, index)
+ pushOrApply(token, HtmlOpenKind.BlockContainer)
+ }
+
+ "hr" -> {
+ val start = builder.length
+ builder.appendText("────────────────────", index)
+ builder.addEditorBlock(EDITOR_DIVIDER_TAG, "", start, builder.length)
+ if (builder.lastChar != '\n') {
+ builder.appendChar('\n', index)
+ }
+ true
+ }
+
+ "tg-emoji" -> {
+ val emojiId = token.attributes["emoji-id"]?.toLongOrNull() ?: return false
+ if (token.selfClosing) {
+ val start = builder.length
+ builder.appendChar('\uFFFC', index)
+ builder.addCustomEmoji(start, builder.length, emojiId)
+ true
+ } else {
+ pushOrApply(token, HtmlOpenKind.CustomEmoji(emojiId))
+ }
+ }
+
+ "tg-math", "math", "latex" -> {
+ ensureBlockBoundary()
+ pushOrApply(token, HtmlOpenKind.Latex(displayMode = name != "latex"))
+ }
+
+ else -> false
+ }
+ }
+
+ private fun closeTag(token: HtmlTagToken): Boolean {
+ val matchIndex = openTags.indexOfLast { it.name == token.name.lowercase() }
+ if (matchIndex == -1) return false
+
+ while (openTags.size > matchIndex) {
+ val openTag = openTags.removeAt(openTags.lastIndex)
+ val start = openTag.start
+ val end = builder.length
+
+ when (val kind = openTag.kind) {
+ is HtmlOpenKind.Rich -> if (start < end) builder.addRichEntity(
+ start,
+ end,
+ kind.type
+ )
+
+ is HtmlOpenKind.Pre -> if (start < end) builder.addRichEntity(
+ start,
+ end,
+ MessageEntityType.Pre(kind.language)
+ )
+
+ is HtmlOpenKind.TextMention -> if (start < end) builder.addMention(
+ start,
+ end,
+ kind.userId
+ )
+
+ is HtmlOpenKind.CustomEmoji -> if (start < end) builder.addCustomEmoji(
+ start,
+ end,
+ kind.emojiId
+ )
+
+ is HtmlOpenKind.Latex -> if (start < end) builder.addLatex(
+ start,
+ end,
+ kind.displayMode
+ )
+
+ is HtmlOpenKind.Heading -> if (start < end) builder.addEditorBlock(
+ EDITOR_HEADING_TAG,
+ kind.level.toString(),
+ start,
+ end
+ )
+
+ is HtmlOpenKind.ListContainer -> Unit
+ HtmlOpenKind.BlockContainer -> Unit
+ }
+
+ if (openTag.name in BLOCK_LEVEL_TAGS && builder.lastChar != '\n') {
+ builder.appendChar('\n', index)
+ }
+
+ if (openTag.name == token.name.lowercase()) break
+ }
+
+ return true
+ }
+
+ private fun pushOrApply(token: HtmlTagToken, kind: HtmlOpenKind): Boolean {
+ if (token.selfClosing) {
+ val start = builder.length
+ when (kind) {
+ is HtmlOpenKind.CustomEmoji -> {
+ builder.appendChar('\uFFFC', index)
+ builder.addCustomEmoji(start, builder.length, kind.emojiId)
+ }
+
+ else -> Unit
+ }
+ return true
+ }
+
+ openTags += HtmlOpenTag(token.name.lowercase(), builder.length, kind)
+ return true
+ }
+
+ private fun ensureBlockBoundary() {
+ if (builder.length > 0 && builder.lastChar != '\n') {
+ builder.appendChar('\n', index)
+ }
+ }
+
+ private fun tryAppendHtmlTable(): Boolean {
+ if (!input.startsWith("", startIndex = index, ignoreCase = true)
+ if (closingStart == -1) return false
+ val closingEnd = closingStart + "
".length
+ val block = input.substring(index, closingEnd)
+ val rows = parseHtmlTableRows(block)
+ if (rows.isEmpty()) return false
+
+ ensureBlockBoundary()
+ val start = builder.length
+ builder.appendText(formatMarkdownTable(rows), index)
+ builder.addRichEntity(start, builder.length, MessageEntityType.Pre("table"))
+ if (closingEnd < input.length && builder.lastChar != '\n') {
+ builder.appendChar('\n', closingStart)
+ }
+ index = closingEnd
+ return true
+ }
+
+ private fun tryHandleEntity(): Boolean {
+ val semicolon = input.indexOf(';', startIndex = index + 1)
+ if (semicolon == -1) return false
+ val entity = input.substring(index + 1, semicolon)
+ val decoded = decodeHtmlEntity(entity) ?: return false
+ decoded.forEach { char -> builder.appendChar(char, index) }
+ index = semicolon + 1
+ return true
+ }
+}
+
+private class ParsedAnnotatedStringBuilder(private val source: AnnotatedString) {
+ private val textBuilder = StringBuilder()
+ private val sourceIndexByOutputIndex = mutableListOf()
+ private val richRanges = mutableListOf()
+ private val mentionRanges = mutableListOf()
+ private val customEmojiRanges = mutableListOf()
+ private val latexRanges = mutableListOf()
+ private val editorBlockRanges = mutableListOf()
+
+ val length: Int
+ get() = textBuilder.length
+
+ val lastChar: Char?
+ get() = textBuilder.lastOrNull()
+
+ fun appendChar(char: Char, sourceIndex: Int) {
+ textBuilder.append(char)
+ sourceIndexByOutputIndex += sourceIndex
+ }
+
+ fun appendText(text: String, sourceIndex: Int) {
+ text.forEach { char -> appendChar(char, sourceIndex) }
+ }
+
+ fun addRichEntity(start: Int, end: Int, type: MessageEntityType) {
+ if (start < end) richRanges += RichRange(start, end, type)
+ }
+
+ fun addMention(start: Int, end: Int, userId: Long) {
+ if (start < end) mentionRanges += MentionRange(start, end, userId)
+ }
+
+ fun addCustomEmoji(start: Int, end: Int, emojiId: Long) {
+ if (start < end) customEmojiRanges += CustomEmojiRange(start, end, emojiId)
+ }
+
+ fun addLatex(start: Int, end: Int, displayMode: Boolean) {
+ if (start < end) latexRanges += LatexRange(start, end, displayMode)
+ }
+
+ fun addEditorBlock(tag: String, item: String, start: Int, end: Int) {
+ if (start < end) editorBlockRanges += EditorBlockRange(tag, item, start, end)
+ }
+
+ fun build(): AnnotatedString {
+ val builder = AnnotatedString.Builder(textBuilder.toString())
+
+ source.getStringAnnotations(0, source.length).forEach { annotation ->
+ val mapped = mapSourceRange(annotation.start, annotation.end) ?: return@forEach
+ if (mapped.first < mapped.second) {
+ builder.addStringAnnotation(
+ annotation.tag,
+ annotation.item,
+ mapped.first,
+ mapped.second
+ )
+ }
+ }
+
+ richRanges.forEach { range ->
+ val key = richEntityToAnnotation(range.type) ?: return@forEach
+ builder.addStringAnnotation(RICH_ENTITY_TAG, key, range.start, range.end)
+ }
+ mentionRanges.forEach { range ->
+ builder.addStringAnnotation(
+ MENTION_TAG,
+ range.userId.toString(),
+ range.start,
+ range.end
+ )
+ }
+ customEmojiRanges.forEach { range ->
+ builder.addStringAnnotation(
+ CUSTOM_EMOJI_TAG,
+ range.emojiId.toString(),
+ range.start,
+ range.end
+ )
+ }
+ latexRanges.forEach { range ->
+ builder.addStringAnnotation(
+ LATEX_TAG,
+ if (range.displayMode) "block" else "inline",
+ range.start,
+ range.end
+ )
+ }
+ editorBlockRanges.forEach { range ->
+ builder.addStringAnnotation(range.tag, range.item, range.start, range.end)
+ }
+
+ return builder.toAnnotatedString()
+ }
+
+ private fun mapSourceRange(start: Int, end: Int): Pair? {
+ var mappedStart = -1
+ var mappedEnd = -1
+
+ sourceIndexByOutputIndex.forEachIndexed { outputIndex, sourceIndex ->
+ if (sourceIndex in start until end) {
+ if (mappedStart == -1) mappedStart = outputIndex
+ mappedEnd = outputIndex + 1
+ }
+ }
+
+ return if (mappedStart >= 0 && mappedEnd > mappedStart) mappedStart to mappedEnd else null
+ }
+}
+
+private fun markerToType(marker: String): MessageEntityType? {
+ return when (marker) {
+ "**" -> MessageEntityType.Bold
+ "__" -> MessageEntityType.Underline
+ "*", "_" -> MessageEntityType.Italic
+ "~~" -> MessageEntityType.Strikethrough
+ "||" -> MessageEntityType.Spoiler
+ "`" -> MessageEntityType.Code
+ else -> null
+ }
+}
+
+private fun normalizeMarkdownForSending(text: String): String {
+ if (text.isBlank()) return text
+ val lines = normalizeMarkdownLatex(text).split('\n')
+ val result = mutableListOf()
+ var index = 0
+
+ while (index < lines.size) {
+ val line = lines[index]
+ val heading = parseMarkdownHeadingLine(line)
+ if (heading != null) {
+ result += "**${heading.trim()}**"
+ index++
+ continue
+ }
+
+ if (isMarkdownDividerLine(line)) {
+ result += "────────────────────"
+ index++
+ continue
+ }
+
+ val table = collectMarkdownTable(lines, index)
+ if (table != null) {
+ result += "```table"
+ result += formatMarkdownTable(table.rows).split('\n')
+ result += "```"
+ index += table.consumedLineCount
+ continue
+ }
+
+ val unordered = MARKDOWN_UNORDERED_LIST_REGEX.find(line)
+ ?.takeIf { it.range.first == 0 }
+ ?.let { match ->
+ "${match.groupValues[1]}\u2022 ${line.substring(match.range.last + 1)}"
+ }
+ ?: line
+ val normalized = MARKDOWN_ORDERED_LIST_REGEX.find(unordered)
+ ?.takeIf { it.range.first == 0 }
+ ?.let { match ->
+ "${match.groupValues[1]}${match.groupValues[2]}. ${unordered.substring(match.range.last + 1)}"
+ }
+ ?: unordered
+ result += normalized
+ index++
+ }
+
+ return result.joinToString("\n")
+}
+
+private fun normalizeHtmlForSending(text: String): String {
+ if (text.isBlank()) return text
+ var normalized = normalizeHtmlMath(text)
+ normalized = normalizeHtmlLists(normalized)
+
+ normalized = HTML_HEADING_REGEX.replace(normalized) { match ->
+ val content = match.groupValues[2].trim()
+ if (content.isBlank()) "" else "$content"
+ }
+
+ normalized = HTML_HR_REGEX.replace(normalized, "────────────────────")
+
+ normalized = HTML_TABLE_REGEX.replace(normalized) { match ->
+ val rows = parseHtmlTableRows(match.value)
+ if (rows.isEmpty()) {
+ match.value
+ } else {
+ "${formatMarkdownTable(rows)}"
+ }
+ }
+
+ return normalized
+}
+
+private fun normalizeMarkdownLatex(text: String): String {
+ val blockNormalized = MARKDOWN_BLOCK_LATEX_REGEX.replace(text) { match ->
+ val content = match.groupValues[1].trim('\n', '\r')
+ if (content.isBlank()) "" else "```math\n$content\n```"
+ }
+ return MARKDOWN_INLINE_LATEX_REGEX.replace(blockNormalized) { match ->
+ val content = match.groupValues[1].trim()
+ if (content.isBlank()) "" else "`$content`"
+ }
+}
+
+private fun normalizeHtmlMath(text: String): String {
+ var normalized = HTML_BLOCK_LATEX_REGEX.replace(text) { match ->
+ val content = decodeBasicHtml(match.groupValues[2].replace(HTML_TAG_REGEX, " ").trim())
+ if (content.isBlank()) "" else "$content
"
+ }
+ normalized = HTML_INLINE_LATEX_REGEX.replace(normalized) { match ->
+ val content = decodeBasicHtml(match.groupValues[2].replace(HTML_TAG_REGEX, " ").trim())
+ if (content.isBlank()) "" else "$content"
+ }
+ return normalized
+}
+
+private fun normalizeHtmlLists(text: String): String {
+ var normalized = text
+ while (true) {
+ val next = HTML_ORDERED_LIST_REGEX.replace(normalized) { match ->
+ val items = extractHtmlListItems(match.groupValues[1])
+ if (items.isEmpty()) {
+ match.value
+ } else {
+ items.mapIndexed { index, item -> "${index + 1}. $item" }.joinToString("\n")
+ }
+ }
+ if (next == normalized) break
+ normalized = next
+ }
+
+ while (true) {
+ val next = HTML_UNORDERED_LIST_REGEX.replace(normalized) { match ->
+ val items = extractHtmlListItems(match.groupValues[1])
+ if (items.isEmpty()) {
+ match.value
+ } else {
+ items.joinToString("\n") { item -> "\u2022 $item" }
+ }
+ }
+ if (next == normalized) break
+ normalized = next
+ }
+
+ return normalized
+}
+
+private fun parseMarkdownHeadingLine(line: String): String? {
+ val match = MARKDOWN_HEADING_LINE_REGEX.find(line) ?: return null
+ if (match.range.first != 0) return null
+ return match.groupValues[3].trim().trimEnd('#').trim()
+}
+
+private fun isMarkdownDividerLine(line: String): Boolean {
+ val trimmed = line.trim()
+ if (trimmed.length < 3) return false
+ val marker = trimmed.firstOrNull() ?: return false
+ if (marker !in setOf('-', '_', '*')) return false
+ if (trimmed.any { it != marker && !it.isWhitespace() }) return false
+ return trimmed.count { it == marker } >= 3
+}
+
+private data class MarkdownTableBlock(
+ val rows: List>,
+ val consumedLineCount: Int
+)
+
+private fun collectMarkdownTable(lines: List, startIndex: Int): MarkdownTableBlock? {
+ if (startIndex + 1 >= lines.size) return null
+ val headerCells = parseMarkdownTableCells(lines[startIndex]) ?: return null
+ val separatorCells =
+ parseMarkdownTableSeparator(lines[startIndex + 1], headerCells.size) ?: return null
+ if (separatorCells.size != headerCells.size) return null
+
+ val rows = mutableListOf(headerCells)
+ var index = startIndex + 2
+ while (index < lines.size) {
+ val row = parseMarkdownTableCells(lines[index]) ?: break
+ if (row.size != headerCells.size) break
+ rows += row
+ index++
+ }
+ return MarkdownTableBlock(rows = rows, consumedLineCount = index - startIndex)
+}
+
+private fun parseMarkdownTableCells(line: String): List? {
+ if (!line.contains('|')) return null
+ val trimmed = line.trim()
+ if (trimmed.isBlank()) return null
+ val normalized = trimmed.removePrefix("|").removeSuffix("|")
+ val cells = splitUnescapedPipes(normalized).map { it.trim() }
+ return if (cells.size >= 2 && cells.any { it.isNotEmpty() }) cells else null
+}
+
+private fun parseMarkdownTableSeparator(line: String, expectedColumns: Int): List? {
+ val cells = parseMarkdownTableCells(line) ?: return null
+ if (cells.size != expectedColumns) return null
+ return cells.takeIf { separatorCells ->
+ separatorCells.all { cell ->
+ val trimmed = cell.trim()
+ trimmed.length >= 3 && trimmed.all { it == '-' || it == ':' }
+ }
+ }
+}
+
+private fun splitUnescapedPipes(line: String): List {
+ val result = mutableListOf()
+ val current = StringBuilder()
+ var escaped = false
+ line.forEach { char ->
+ when {
+ escaped -> {
+ current.append(char)
+ escaped = false
+ }
+
+ char == '\\' -> escaped = true
+ char == '|' -> {
+ result += current.toString()
+ current.clear()
+ }
+
+ else -> current.append(char)
+ }
+ }
+ result += current.toString()
+ return result
+}
+
+private fun formatMarkdownTable(rows: List>): String {
+ if (rows.isEmpty()) return ""
+ val columnCount = rows.maxOfOrNull { it.size } ?: return ""
+ val normalizedRows = rows.map { row ->
+ if (row.size == columnCount) row else row + List(columnCount - row.size) { "" }
+ }
+ val widths = IntArray(columnCount) { column ->
+ normalizedRows.maxOf { row -> row[column].length }
+ }
+
+ fun border(left: String, middle: String, right: String): String {
+ return buildString {
+ append(left)
+ widths.forEachIndexed { index, width ->
+ append("─".repeat(width + 2))
+ append(if (index == widths.lastIndex) right else middle)
+ }
+ }
+ }
+
+ fun rowLine(cells: List): String {
+ return buildString {
+ append("│")
+ cells.forEachIndexed { index, cell ->
+ append(' ')
+ append(cell.padEnd(widths[index]))
+ append(' ')
+ append("│")
+ }
+ }
+ }
+
+ return buildString {
+ appendLine(border("┌", "┬", "┐"))
+ appendLine(rowLine(normalizedRows.first()))
+ appendLine(border("├", "┼", "┤"))
+ normalizedRows.drop(1).forEachIndexed { index, row ->
+ appendLine(rowLine(row))
+ if (index != normalizedRows.drop(1).lastIndex) {
+ appendLine(border("├", "┼", "┤"))
+ }
+ }
+ append(border("└", "┴", "┘"))
+ }
+}
+
+private fun parseHtmlTableRows(tableHtml: String): List> {
+ val rowMatches = HTML_TABLE_ROW_REGEX.findAll(tableHtml)
+ return rowMatches.mapNotNull { rowMatch ->
+ val cells = HTML_TABLE_CELL_REGEX.findAll(rowMatch.groupValues[1]).map { cellMatch ->
+ decodeBasicHtml(cellMatch.groupValues[2].replace(HTML_TAG_REGEX, " ").trim())
+ }.toList()
+ cells.takeIf { it.isNotEmpty() }
+ }.toList()
+}
+
+private fun extractHtmlListItems(listHtml: String): List {
+ return HTML_LIST_ITEM_REGEX.findAll(listHtml).mapNotNull { match ->
+ decodeBasicHtml(match.groupValues[1].replace(HTML_TAG_REGEX, " ").trim())
+ .takeIf { it.isNotBlank() }
+ }.toList()
+}
+
+private fun decodeBasicHtml(value: String): String {
+ return value
+ .replace(" ", " ", ignoreCase = true)
+ .replace("<", "<", ignoreCase = true)
+ .replace(">", ">", ignoreCase = true)
+ .replace("&", "&", ignoreCase = true)
+ .replace(""", "\"", ignoreCase = true)
+ .replace("'", "'", ignoreCase = true)
+}
+
+private fun parseHtmlTagToken(text: String, startIndex: Int): HtmlTagToken? {
+ if (text.getOrNull(startIndex) != '<') return null
+
+ var cursor = startIndex + 1
+ var inSingleQuote = false
+ var inDoubleQuote = false
+
+ while (cursor < text.length) {
+ when (text[cursor]) {
+ '\'' -> if (!inDoubleQuote) inSingleQuote = !inSingleQuote
+ '"' -> if (!inSingleQuote) inDoubleQuote = !inDoubleQuote
+ '>' -> if (!inSingleQuote && !inDoubleQuote) break
+ }
+ cursor++
+ }
+
+ if (cursor >= text.length || text[cursor] != '>') return null
+
+ val raw = text.substring(startIndex, cursor + 1)
+ val inner = raw.removePrefix("<").removeSuffix(">").trim()
+ if (inner.isEmpty()) return null
+
+ val isClosing = inner.startsWith("/")
+ val normalized = if (isClosing) inner.removePrefix("/").trim() else inner
+ val selfClosing = normalized.endsWith("/")
+ val content = normalized.removeSuffix("/").trim()
+ if (content.isEmpty()) return null
+
+ val nameEnd =
+ content.indexOfFirst { it.isWhitespace() }.let { if (it == -1) content.length else it }
+ val name = content.substring(0, nameEnd)
+ val attributes = if (nameEnd < content.length) {
+ parseHtmlAttributes(content.substring(nameEnd).trim())
+ } else {
+ emptyMap()
+ }
+
+ return HtmlTagToken(
+ raw = raw,
+ name = name,
+ isClosing = isClosing,
+ selfClosing = selfClosing,
+ attributes = attributes
+ )
+}
+
+private fun parseHtmlAttributes(input: String): Map {
+ if (input.isBlank()) return emptyMap()
+ val result = linkedMapOf()
+ var index = 0
+
+ while (index < input.length) {
+ while (index < input.length && input[index].isWhitespace()) index++
+ if (index >= input.length) break
+
+ val nameStart = index
+ while (index < input.length && !input[index].isWhitespace() && input[index] != '=') index++
+ val name = input.substring(nameStart, index).lowercase()
+ while (index < input.length && input[index].isWhitespace()) index++
+
+ var value: String? = null
+ if (index < input.length && input[index] == '=') {
+ index++
+ while (index < input.length && input[index].isWhitespace()) index++
+ if (index >= input.length) {
+ value = ""
+ } else if (input[index] == '"' || input[index] == '\'') {
+ val quote = input[index]
+ index++
+ val valueStart = index
+ while (index < input.length && input[index] != quote) index++
+ value = input.substring(valueStart, index)
+ if (index < input.length) index++
+ } else {
+ val valueStart = index
+ while (index < input.length && !input[index].isWhitespace()) index++
+ value = input.substring(valueStart, index)
+ }
+ }
+
+ if (name.isNotBlank()) {
+ result[name] = value
+ }
+ }
+
+ return result
+}
+
+private fun decodeHtmlEntity(entity: String): String? {
+ return when (entity.lowercase()) {
+ "amp" -> "&"
+ "lt" -> "<"
+ "gt" -> ">"
+ "quot" -> "\""
+ "apos", "#39" -> "'"
+ "nbsp" -> " "
+ else -> {
+ when {
+ entity.startsWith("#x", ignoreCase = true) -> entity.substring(2).toIntOrNull(16)
+ ?.toChar()?.toString()
+
+ entity.startsWith("#") -> entity.substring(1).toIntOrNull()?.toChar()?.toString()
+ else -> null
+ }
+ }
+ }
+}
+
+private data class MarkdownQuote(
+ val start: Int,
+ val type: MessageEntityType
+)
+
+private data class MarkdownHeading(
+ val start: Int,
+ val level: Int
+)
+
+private data class RichRange(
+ val start: Int,
+ val end: Int,
+ val type: MessageEntityType
+)
+
+private data class MentionRange(
+ val start: Int,
+ val end: Int,
+ val userId: Long
+)
+
+private data class CustomEmojiRange(
+ val start: Int,
+ val end: Int,
+ val emojiId: Long
+)
+
+private data class LatexRange(
+ val start: Int,
+ val end: Int,
+ val displayMode: Boolean
+)
+
+private data class EditorBlockRange(
+ val tag: String,
+ val item: String,
+ val start: Int,
+ val end: Int
+)
+
+private data class HtmlTagToken(
+ val raw: String,
+ val name: String,
+ val isClosing: Boolean,
+ val selfClosing: Boolean,
+ val attributes: Map
+)
+
+private data class HtmlOpenTag(
+ val name: String,
+ val start: Int,
+ var kind: HtmlOpenKind
+)
+
+private sealed interface HtmlOpenKind {
+ data class Rich(val type: MessageEntityType) : HtmlOpenKind
+ data class Pre(val language: String) : HtmlOpenKind
+ data class Heading(val level: Int) : HtmlOpenKind
+ data class ListContainer(val ordered: Boolean, var nextIndex: Int = 1) : HtmlOpenKind
+ data class TextMention(val userId: Long) : HtmlOpenKind
+ data class CustomEmoji(val emojiId: Long) : HtmlOpenKind
+ data class Latex(val displayMode: Boolean) : HtmlOpenKind
+ data object BlockContainer : HtmlOpenKind
+}
+
+private val LINK_REGEX = Regex("\\[([^\\]]+)]\\(([^)]+)\\)")
+private val MARKDOWN_MARKERS = listOf("**", "__", "~~", "||", "`", "*", "_")
+private val CODE_ONLY_MARKERS = listOf("`")
+private val MARKDOWN_HEADING_LINE_REGEX = Regex("^(\\s{0,3})(#{1,3})\\s+(.+?)\\s*$")
+private val MARKDOWN_UNORDERED_LIST_REGEX = Regex("^(\\s{0,6})[-*+]\\s+")
+private val MARKDOWN_ORDERED_LIST_REGEX = Regex("^(\\s{0,6})(\\d+)[.)]\\s+")
+private val BLOCK_LEVEL_TAGS =
+ setOf("pre", "blockquote", "p", "div", "tg-math", "math", "h1", "h2", "h3", "ul", "ol", "li")
+private val HTML_HEADING_REGEX = Regex(
+ "]*>(.*?)",
+ setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL)
+)
+private val HTML_HR_REGEX = Regex("
]*?/?>", RegexOption.IGNORE_CASE)
+private val HTML_UNORDERED_LIST_REGEX =
+ Regex("", setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL))
+private val HTML_ORDERED_LIST_REGEX =
+ Regex("]*>(.*?)
", setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL))
+private val HTML_LIST_ITEM_REGEX =
+ Regex("]*>(.*?)", setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL))
+private val HTML_TABLE_REGEX =
+ Regex("", setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL))
+private val HTML_TABLE_ROW_REGEX =
+ Regex("]*>(.*?)
", setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL))
+private val HTML_TABLE_CELL_REGEX = Regex(
+ "<(th|td)\\b[^>]*>(.*?)\\1>",
+ setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL)
+)
+private val HTML_TAG_REGEX = Regex("<[^>]+>")
+private val HTML_BLOCK_LATEX_REGEX = Regex(
+ "<(tg-math|math)\\b[^>]*>(.*?)\\1>",
+ setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL)
+)
+private val HTML_INLINE_LATEX_REGEX = Regex(
+ "]*>(.*?)",
+ setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL)
+)
+private val MARKDOWN_BLOCK_LATEX_REGEX = Regex("\\$\\$\\s*([\\s\\S]*?)\\s*\\$\\$")
+private val MARKDOWN_INLINE_LATEX_REGEX = Regex("(? {
+ return AI_PANEL_LANGUAGE_CODES.distinct().map { code ->
+ val label = Locale(code).getDisplayLanguage(Locale.getDefault()).ifBlank { code }
+ AiPanelLanguageOption(
+ code = code,
+ label = label.replaceFirstChar { char -> char.titlecase(Locale.getDefault()) }
+ )
+ }
+}
+
+@Composable
+private fun resolveAiPanelStyleTitle(style: TextCompositionStyleModel): String {
+ return when (style.name.lowercase(Locale.ROOT)) {
+ "formal" -> stringResource(R.string.editor_ai_style_formal)
+ "short" -> stringResource(R.string.editor_ai_style_short)
+ "tribal" -> stringResource(R.string.editor_ai_style_tribal)
+ "corp" -> stringResource(R.string.editor_ai_style_corp)
+ "biblical" -> stringResource(R.string.editor_ai_style_biblical)
+ "viking" -> stringResource(R.string.editor_ai_style_viking)
+ "zen" -> stringResource(R.string.editor_ai_style_zen)
+ else -> style.title.ifBlank { style.name }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+internal fun FullScreenEditorAiSheetContent(
+ visible: Boolean,
+ mode: AiEditorMode,
+ loading: Boolean,
+ errorMessage: String?,
+ styles: List,
+ stickerRepository: StickerRepository,
+ selectedStyleName: String,
+ translateLanguage: String,
+ prompt: String,
+ addEmojis: Boolean,
+ emojiFontFamily: FontFamily,
+ inlineContent: Map,
+ originalText: String,
+ resultText: AnnotatedString?,
+ resultPlainText: String?,
+ showDiffMode: Boolean,
+ supportsPromptBasedAi: Boolean,
+ onDismiss: () -> Unit,
+ onModeChange: (AiEditorMode) -> Unit,
+ onToggleDiffMode: () -> Unit,
+ onStyleSelected: (String) -> Unit,
+ onTranslateLanguageChange: (String) -> Unit,
+ onPromptChange: (String) -> Unit,
+ onAddEmojisChange: (Boolean) -> Unit,
+ onRun: () -> Unit,
+ onApplyResult: () -> Unit
+) {
+ if (!visible) return
+
+ val languageOptions = remember { buildAiPanelLanguageOptions() }
+ val fallbackLanguageCode = LocalLocale.current.platformLocale.language.ifBlank { "en" }
+ val selectedLanguageCode = translateLanguage.ifBlank { fallbackLanguageCode }
+ val selectedLanguage = languageOptions.firstOrNull { it.code == selectedLanguageCode }
+ var languageMenuExpanded by remember { mutableStateOf(false) }
+ val customEmojiStickerSets by stickerRepository.customEmojiStickerSets.collectAsState()
+ val styleEmojiFileIds = remember(customEmojiStickerSets) {
+ buildMap {
+ customEmojiStickerSets.forEach { set ->
+ set.stickers.forEach { sticker ->
+ val customEmojiId = sticker.customEmojiId
+ if (customEmojiId != null && customEmojiId != 0L) {
+ put(customEmojiId, sticker.id)
+ }
+ }
+ }
+ }
+ }
+ val effectiveStyles = if (styles.isEmpty()) DEFAULT_AI_PANEL_STYLES else styles
+ val modeOptions = listOf(
+ AiPanelModeOption(
+ mode = AiEditorMode.Translate,
+ label = stringResource(R.string.editor_ai_tab_translate),
+ icon = Icons.Outlined.Translate
+ ),
+ if (effectiveStyles.isNotEmpty()) {
+ AiPanelModeOption(
+ mode = AiEditorMode.Stylize,
+ label = stringResource(R.string.editor_ai_tab_stylize),
+ icon = Icons.Outlined.AutoAwesome
+ )
+ } else {
+ null
+ },
+ if (supportsPromptBasedAi) {
+ AiPanelModeOption(
+ mode = AiEditorMode.Generate,
+ label = stringResource(R.string.editor_ai_tab_generate),
+ icon = Icons.Outlined.Description
+ )
+ } else {
+ null
+ },
+ AiPanelModeOption(
+ mode = AiEditorMode.Fix,
+ label = stringResource(R.string.editor_ai_tab_fix),
+ icon = Icons.Outlined.FormatClear
+ )
+ ).filterNotNull()
+ val sectionTitleStyle = MaterialTheme.typography.labelLarge
+ val addedDiffColor = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.8f)
+ val removedDiffColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.8f)
+ val diffText = remember(originalText, resultPlainText, addedDiffColor, removedDiffColor) {
+ if (resultPlainText.isNullOrBlank()) {
+ null
+ } else {
+ calculateDiff(
+ old = originalText,
+ new = resultPlainText,
+ addedColor = addedDiffColor,
+ removedColor = removedDiffColor
+ )
+ }
+ }
+ val sourcePreviewTitle = if (mode == AiEditorMode.Generate && originalText.isBlank()) {
+ stringResource(R.string.editor_ai_prompt_label)
+ } else {
+ stringResource(R.string.editor_ai_original)
+ }
+ val sourcePreviewText = when {
+ mode == AiEditorMode.Generate && prompt.isBlank() -> stringResource(R.string.editor_ai_prompt_placeholder)
+ mode == AiEditorMode.Generate && originalText.isBlank() -> prompt
+ else -> originalText
+ }
+ val formattedSourceText = remember(sourcePreviewText, emojiFontFamily) {
+ buildEmojiText(sourcePreviewText, emojiFontFamily)
+ }
+ val formattedDiffText = remember(diffText, emojiFontFamily) {
+ diffText?.let { buildEmojiText(it, emojiFontFamily) }
+ }
+ val formattedResultText = remember(resultText, emojiFontFamily) {
+ resultText?.let { buildEmojiText(it, emojiFontFamily) }
+ }
+ val actionButtonText = if (resultText != null) {
+ stringResource(R.string.editor_ai_apply_result)
+ } else {
+ when (mode) {
+ AiEditorMode.Translate -> stringResource(R.string.editor_ai_run_translate)
+ AiEditorMode.Stylize -> stringResource(R.string.editor_ai_run_stylize)
+ AiEditorMode.Generate -> stringResource(R.string.editor_ai_run_generate)
+ AiEditorMode.Fix -> stringResource(R.string.editor_ai_run_fix)
+ }
+ }
+
+ LaunchedEffect(styles, mode, selectedStyleName) {
+ if (styles.isEmpty() && mode == AiEditorMode.Stylize) {
+ onModeChange(AiEditorMode.Translate)
+ return@LaunchedEffect
+ }
+
+ if (mode == AiEditorMode.Stylize && styles.isNotEmpty() && styles.none { it.name == selectedStyleName }) {
+ onStyleSelected(styles.first().name)
+ }
+ }
+
+ LaunchedEffect(styles) {
+ if (styles.any { it.customEmojiId != 0L }) {
+ runCatching { stickerRepository.loadCustomEmojiStickerSets() }
+ }
+ }
+
+ LaunchedEffect(supportsPromptBasedAi, mode) {
+ if (!supportsPromptBasedAi && mode == AiEditorMode.Generate) {
+ onModeChange(AiEditorMode.Translate)
+ }
+ }
+
+ ModalBottomSheet(
+ onDismissRequest = onDismiss,
+ dragHandle = { BottomSheetDefaults.DragHandle() },
+ containerColor = MaterialTheme.colorScheme.background,
+ contentColor = MaterialTheme.colorScheme.onSurface,
+ sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
+ shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp)
+ .navigationBarsPadding()
+ .imePadding()
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f, fill = false)
+ .animateContentSize(),
+ verticalArrangement = Arrangement.spacedBy(14.dp)
+ ) {
+ Surface(
+ modifier = Modifier.fillMaxWidth(),
+ color = MaterialTheme.colorScheme.surfaceContainer,
+ shape = RoundedCornerShape(28.dp)
+ ) {
+ Row(
+ modifier = Modifier.padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Surface(
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.primaryContainer
+ ) {
+ Box(
+ modifier = Modifier.size(40.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.AutoAwesome,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onPrimaryContainer,
+ modifier = Modifier.size(20.dp)
+ )
+ }
+ }
+
+ Column(
+ modifier = Modifier
+ .weight(1f)
+ .padding(start = 12.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.editor_ai_title),
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.SemiBold
+ )
+ Text(
+ text = stringResource(R.string.editor_ai_subtitle),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+
+ IconButton(onClick = onDismiss) {
+ Icon(
+ imageVector = Icons.Outlined.Close,
+ contentDescription = stringResource(R.string.action_close)
+ )
+ }
+ }
+ }
+
+ Surface(
+ modifier = Modifier.fillMaxWidth(),
+ color = MaterialTheme.colorScheme.surfaceContainer,
+ shape = RoundedCornerShape(24.dp)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(12.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ AiModeOptionsGrid(
+ options = modeOptions,
+ selectedMode = mode,
+ loading = loading,
+ onModeChange = onModeChange
+ )
+ }
+ }
+
+ AnimatedContent(targetState = mode, label = "ai_mode_content") { currentMode ->
+ when (currentMode) {
+ AiEditorMode.Translate -> {
+ Surface(
+ modifier = Modifier.fillMaxWidth(),
+ color = MaterialTheme.colorScheme.surfaceContainer,
+ shape = RoundedCornerShape(24.dp)
+ ) {
+ Column(
+ modifier = Modifier.padding(12.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ Box {
+ Surface(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = { if (!loading) languageMenuExpanded = true },
+ enabled = !loading,
+ shape = RoundedCornerShape(16.dp),
+ color = MaterialTheme.colorScheme.surfaceContainerHighest
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(52.dp)
+ .padding(horizontal = 14.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.Translate,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.size(18.dp)
+ )
+ Column(
+ modifier = Modifier
+ .weight(1f)
+ .padding(start = 10.dp)
+ ) {
+ Text(
+ text = selectedLanguage?.label
+ ?: selectedLanguageCode,
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurface,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ Text(
+ text = stringResource(R.string.editor_ai_select_language),
+ style = MaterialTheme.typography.labelSmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ Icon(
+ imageVector = Icons.Outlined.KeyboardArrowDown,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.size(18.dp)
+ )
+ }
+ }
+
+ DropdownMenu(
+ expanded = languageMenuExpanded,
+ onDismissRequest = { languageMenuExpanded = false },
+ shape = RoundedCornerShape(16.dp),
+ tonalElevation = 0.dp,
+ shadowElevation = 0.dp
+ ) {
+ languageOptions.forEach { option ->
+ val selected = option.code == selectedLanguageCode
+ DropdownMenuItem(
+ text = { Text(option.label) },
+ leadingIcon = {
+ Icon(
+ imageVector = Icons.Outlined.Translate,
+ contentDescription = null
+ )
+ },
+ trailingIcon = if (selected) {
+ {
+ Icon(
+ imageVector = Icons.Filled.Check,
+ contentDescription = null
+ )
+ }
+ } else null,
+ onClick = {
+ onTranslateLanguageChange(option.code)
+ languageMenuExpanded = false
+ }
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+
+ AiEditorMode.Generate -> {
+ Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
+ SettingsTextField(
+ value = prompt,
+ onValueChange = onPromptChange,
+ placeholder = stringResource(R.string.editor_ai_prompt_placeholder),
+ icon = Icons.Outlined.Description,
+ position = ItemPosition.STANDALONE,
+ modifier = Modifier.fillMaxWidth(),
+ containerColor = MaterialTheme.colorScheme.surfaceContainer,
+ minLines = 3,
+ maxLines = 5
+ )
+
+ Surface(
+ modifier = Modifier.fillMaxWidth(),
+ color = MaterialTheme.colorScheme.surfaceContainer,
+ shape = RoundedCornerShape(24.dp)
+ ) {
+ Column(
+ modifier = Modifier.padding(12.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ Box {
+ Surface(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = {
+ if (!loading) languageMenuExpanded = true
+ },
+ enabled = !loading,
+ shape = RoundedCornerShape(16.dp),
+ color = MaterialTheme.colorScheme.surfaceContainerHighest
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(52.dp)
+ .padding(horizontal = 14.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.Translate,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.size(18.dp)
+ )
+ Column(
+ modifier = Modifier
+ .weight(1f)
+ .padding(start = 10.dp)
+ ) {
+ Text(
+ text = selectedLanguage?.label
+ ?: selectedLanguageCode,
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurface,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ Text(
+ text = stringResource(R.string.editor_ai_select_language),
+ style = MaterialTheme.typography.labelSmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ Icon(
+ imageVector = Icons.Outlined.KeyboardArrowDown,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.size(18.dp)
+ )
+ }
+ }
+
+ DropdownMenu(
+ expanded = languageMenuExpanded,
+ onDismissRequest = { languageMenuExpanded = false },
+ shape = RoundedCornerShape(16.dp),
+ tonalElevation = 0.dp,
+ shadowElevation = 0.dp
+ ) {
+ languageOptions.forEach { option ->
+ val selected =
+ option.code == selectedLanguageCode
+ DropdownMenuItem(
+ text = { Text(option.label) },
+ leadingIcon = {
+ Icon(
+ imageVector = Icons.Outlined.Translate,
+ contentDescription = null
+ )
+ },
+ trailingIcon = if (selected) {
+ {
+ Icon(
+ imageVector = Icons.Filled.Check,
+ contentDescription = null
+ )
+ }
+ } else null,
+ onClick = {
+ onTranslateLanguageChange(option.code)
+ languageMenuExpanded = false
+ }
+ )
+ }
+ }
+ }
+
+ AiCompactToggleTile(
+ icon = Icons.Outlined.EmojiEmotions,
+ title = stringResource(R.string.editor_ai_add_emojis),
+ checked = addEmojis,
+ iconColor = MaterialTheme.colorScheme.primary,
+ enabled = !loading,
+ onCheckedChange = onAddEmojisChange
+ )
+ }
+ }
+ }
+ }
+
+ AiEditorMode.Stylize -> {
+ Surface(
+ modifier = Modifier.fillMaxWidth(),
+ color = MaterialTheme.colorScheme.surfaceContainer,
+ shape = RoundedCornerShape(24.dp)
+ ) {
+ Column(
+ modifier = Modifier.padding(12.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ if (effectiveStyles.isNotEmpty()) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .horizontalScroll(rememberScrollState()),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ effectiveStyles.forEach { style ->
+ AiStyleChip(
+ selected = style.name == selectedStyleName,
+ onClick = { onStyleSelected(style.name) },
+ label = resolveAiPanelStyleTitle(style),
+ emojiFileId = styleEmojiFileIds[style.customEmojiId],
+ enabled = !loading,
+ stickerRepository = stickerRepository
+ )
+ }
+ }
+ }
+
+ AiCompactToggleTile(
+ icon = Icons.Outlined.EmojiEmotions,
+ title = stringResource(R.string.editor_ai_add_emojis),
+ checked = addEmojis,
+ iconColor = MaterialTheme.colorScheme.primary,
+ enabled = !loading,
+ onCheckedChange = onAddEmojisChange
+ )
+ }
+ }
+ }
+
+ AiEditorMode.Fix -> Unit
+ }
+ }
+
+ AnimatedContent(
+ targetState = resultText != null,
+ label = "ai_result_visibility"
+ ) { hasResult ->
+ if (!hasResult) {
+ Surface(
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(24.dp),
+ color = MaterialTheme.colorScheme.surfaceContainer
+ ) {
+ Column(
+ modifier = Modifier.padding(12.dp),
+ verticalArrangement = Arrangement.spacedBy(6.dp)
+ ) {
+ Text(
+ text = sourcePreviewTitle,
+ style = sectionTitleStyle,
+ color = MaterialTheme.colorScheme.primary,
+ fontWeight = FontWeight.SemiBold
+ )
+ Text(
+ text = formattedSourceText,
+ style = MaterialTheme.typography.bodyMedium,
+ maxLines = 4,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ }
+ } else {
+ AnimatedContent(
+ targetState = showDiffMode,
+ label = "ai_result_view_mode"
+ ) { isDiffMode ->
+ if (isDiffMode && formattedDiffText != null) {
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .heightIn(max = 300.dp),
+ shape = RoundedCornerShape(24.dp),
+ color = MaterialTheme.colorScheme.surfaceContainer
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(12.dp),
+ verticalArrangement = Arrangement.spacedBy(6.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = stringResource(R.string.editor_ai_changes),
+ style = sectionTitleStyle,
+ color = MaterialTheme.colorScheme.primary,
+ fontWeight = FontWeight.SemiBold,
+ modifier = Modifier.weight(1f)
+ )
+ IconButton(
+ onClick = onToggleDiffMode,
+ modifier = Modifier.size(40.dp)
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.VisibilityOff,
+ contentDescription = stringResource(R.string.editor_ai_changes),
+ tint = MaterialTheme.colorScheme.primary
+ )
+ }
+ }
+ Text(
+ text = formattedDiffText,
+ style = MaterialTheme.typography.bodyMedium
+ )
+ }
+ }
+ } else {
+ Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ Surface(
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(24.dp),
+ color = MaterialTheme.colorScheme.surfaceContainer
+ ) {
+ Column(
+ modifier = Modifier.padding(12.dp),
+ verticalArrangement = Arrangement.spacedBy(6.dp)
+ ) {
+ Text(
+ text = sourcePreviewTitle,
+ style = sectionTitleStyle,
+ color = MaterialTheme.colorScheme.primary,
+ fontWeight = FontWeight.SemiBold
+ )
+ Text(
+ text = formattedSourceText,
+ style = MaterialTheme.typography.bodyMedium,
+ maxLines = 6,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ }
+
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .heightIn(max = 300.dp),
+ shape = RoundedCornerShape(24.dp),
+ color = MaterialTheme.colorScheme.surfaceContainer
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(12.dp),
+ verticalArrangement = Arrangement.spacedBy(6.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = stringResource(R.string.editor_ai_result),
+ style = sectionTitleStyle,
+ color = MaterialTheme.colorScheme.primary,
+ fontWeight = FontWeight.SemiBold,
+ modifier = Modifier.weight(1f)
+ )
+ IconButton(
+ onClick = onToggleDiffMode,
+ modifier = Modifier.size(40.dp)
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.Visibility,
+ contentDescription = stringResource(R.string.editor_ai_changes),
+ tint = MaterialTheme.colorScheme.primary
+ )
+ }
+ }
+ Text(
+ text = formattedResultText ?: AnnotatedString(""),
+ inlineContent = inlineContent,
+ style = MaterialTheme.typography.bodyMedium
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ AnimatedVisibility(
+ visible = !errorMessage.isNullOrBlank(),
+ enter = fadeIn() + expandVertically(),
+ exit = fadeOut() + shrinkVertically()
+ ) {
+ Text(
+ text = errorMessage.orEmpty(),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.error
+ )
+ }
+
+ AnimatedVisibility(
+ visible = loading,
+ enter = fadeIn() + expandVertically(),
+ exit = fadeOut() + shrinkVertically()
+ ) {
+ Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
+ LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
+ Text(
+ text = stringResource(R.string.editor_ai_loading),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ }
+
+ Spacer(modifier = Modifier.height(14.dp))
+
+ Button(
+ onClick = {
+ if (resultText != null) onApplyResult() else onRun()
+ },
+ enabled = !loading,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(52.dp),
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ Text(text = actionButtonText, fontWeight = FontWeight.Bold)
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+ }
+}
+
+@Composable
+private fun AiStyleChip(
+ selected: Boolean,
+ onClick: () -> Unit,
+ label: String,
+ emojiFileId: Long?,
+ enabled: Boolean,
+ stickerRepository: StickerRepository
+) {
+ val emojiPath by if (emojiFileId != null) {
+ stickerRepository.getStickerFile(emojiFileId).collectAsState(initial = null)
+ } else {
+ remember(emojiFileId) { mutableStateOf(null) }
+ }
+
+ val backgroundColor = if (selected) {
+ MaterialTheme.colorScheme.primaryContainer
+ } else {
+ MaterialTheme.colorScheme.surfaceContainerHigh
+ }
+ val contentColor = if (selected) {
+ MaterialTheme.colorScheme.onPrimaryContainer
+ } else {
+ MaterialTheme.colorScheme.onSurface
+ }
+
+ Surface(
+ onClick = onClick,
+ enabled = enabled,
+ shape = RoundedCornerShape(16.dp),
+ color = backgroundColor
+ ) {
+ Row(
+ modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp),
+ horizontalArrangement = Arrangement.spacedBy(6.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ if (emojiFileId != null) {
+ if (emojiPath != null) {
+ StickerImage(
+ path = emojiPath,
+ modifier = Modifier.size(18.dp),
+ animate = true
+ )
+ } else {
+ Icon(
+ imageVector = Icons.Outlined.AutoAwesome,
+ contentDescription = null,
+ tint = contentColor,
+ modifier = Modifier.size(14.dp)
+ )
+ }
+ }
+
+ Text(
+ text = label,
+ style = MaterialTheme.typography.labelLarge,
+ color = contentColor,
+ fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Medium
+ )
+ }
+ }
+}
+
+@Composable
+private fun AiModeOptionsGrid(
+ options: List,
+ selectedMode: AiEditorMode,
+ loading: Boolean,
+ onModeChange: (AiEditorMode) -> Unit
+) {
+ BoxWithConstraints(
+ modifier = Modifier
+ .fillMaxWidth()
+ .selectableGroup()
+ ) {
+ val columns = when {
+ maxWidth < 360.dp -> 2
+ maxWidth < 520.dp -> 3
+ else -> 4
+ }
+
+ Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ options.chunked(columns).forEach { rowOptions ->
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ rowOptions.forEach { option ->
+ val selected = selectedMode == option.mode
+ Surface(
+ modifier = Modifier
+ .weight(1f)
+ .heightIn(min = 64.dp),
+ onClick = { if (!loading) onModeChange(option.mode) },
+ enabled = !loading,
+ shape = RoundedCornerShape(18.dp),
+ color = if (selected) {
+ MaterialTheme.colorScheme.primaryContainer
+ } else {
+ MaterialTheme.colorScheme.surfaceContainerHigh
+ }
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp, vertical = 8.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ Icon(
+ imageVector = option.icon,
+ contentDescription = null,
+ tint = if (selected) {
+ MaterialTheme.colorScheme.onPrimaryContainer
+ } else {
+ MaterialTheme.colorScheme.onSurfaceVariant
+ },
+ modifier = Modifier.size(18.dp)
+ )
+ Text(
+ text = option.label,
+ style = MaterialTheme.typography.labelMedium,
+ color = if (selected) {
+ MaterialTheme.colorScheme.onPrimaryContainer
+ } else {
+ MaterialTheme.colorScheme.onSurfaceVariant
+ },
+ fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Medium,
+ textAlign = TextAlign.Center,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ }
+ }
+
+ repeat(columns - rowOptions.size) {
+ Spacer(modifier = Modifier.weight(1f))
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun AiCompactToggleTile(
+ icon: ImageVector,
+ title: String,
+ checked: Boolean,
+ iconColor: androidx.compose.ui.graphics.Color,
+ enabled: Boolean,
+ onCheckedChange: (Boolean) -> Unit
+) {
+ val containerColor = if (checked) {
+ MaterialTheme.colorScheme.primaryContainer
+ } else {
+ MaterialTheme.colorScheme.surfaceContainerHigh
+ }
+ val contentColor = if (checked) {
+ MaterialTheme.colorScheme.onPrimaryContainer
+ } else {
+ MaterialTheme.colorScheme.onSurfaceVariant
+ }
+
+ Surface(
+ color = containerColor,
+ shape = RoundedCornerShape(18.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 2.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .clickable(
+ enabled = enabled,
+ interactionSource = remember { MutableInteractionSource() },
+ indication = null,
+ ) { onCheckedChange(!checked) }
+ .padding(horizontal = 12.dp, vertical = 8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Box(
+ modifier = Modifier
+ .size(40.dp)
+ .clip(CircleShape),
+ contentAlignment = Alignment.Center
+ ) {
+ Box(
+ modifier = Modifier
+ .size(28.dp)
+ .background(
+ color = if (checked) {
+ MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.12f)
+ } else {
+ MaterialTheme.colorScheme.surfaceContainerHighest
+ },
+ shape = CircleShape
+ ),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = null,
+ tint = if (checked) {
+ MaterialTheme.colorScheme.onPrimaryContainer
+ } else {
+ iconColor
+ },
+ modifier = Modifier.size(16.dp)
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.width(10.dp))
+
+ Text(
+ text = title,
+ style = MaterialTheme.typography.bodyMedium,
+ color = contentColor,
+ modifier = Modifier.weight(1f)
+ )
+ }
+ }
+}
+
+private fun buildEmojiText(text: String, emojiFontFamily: FontFamily): AnnotatedString {
+ return buildAnnotatedString {
+ append(text)
+ if (emojiFontFamily != FontFamily.Default) {
+ addEmojiStyle(text, emojiFontFamily)
+ }
+ }
+}
+
+private fun buildEmojiText(text: AnnotatedString, emojiFontFamily: FontFamily): AnnotatedString {
+ return if (emojiFontFamily == FontFamily.Default) {
+ text
+ } else {
+ buildAnnotatedString {
+ append(text)
+ addEmojiStyle(text.text, emojiFontFamily)
+ }
+ }
+}
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorChrome.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorChrome.kt
new file mode 100644
index 000000000..16dc704e5
--- /dev/null
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorChrome.kt
@@ -0,0 +1,325 @@
+package org.monogram.presentation.features.chats.conversation.ui.inputbar
+
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.automirrored.filled.Redo
+import androidx.compose.material.icons.automirrored.filled.Undo
+import androidx.compose.material.icons.automirrored.outlined.Subject
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material.icons.outlined.AutoAwesome
+import androidx.compose.material.icons.outlined.Code
+import androidx.compose.material.icons.outlined.Description
+import androidx.compose.material.icons.outlined.KeyboardArrowDown
+import androidx.compose.material.icons.outlined.Search
+import androidx.compose.material.icons.outlined.TextFields
+import androidx.compose.material.icons.outlined.Visibility
+import androidx.compose.material.icons.outlined.VisibilityOff
+import androidx.compose.material.icons.outlined.ZoomIn
+import androidx.compose.material.icons.outlined.ZoomOut
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import org.monogram.presentation.R
+
+@Composable
+internal fun FullScreenEditorHeader(
+ isOverLimit: Boolean,
+ isSending: Boolean,
+ canUndo: Boolean,
+ canRedo: Boolean,
+ onDismiss: () -> Unit,
+ onUndo: () -> Unit,
+ onRedo: () -> Unit,
+ onSend: () -> Unit
+) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ FullScreenEditorToolbarActionButton(
+ icon = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = stringResource(R.string.action_cancel),
+ onClick = onDismiss
+ )
+
+ Box(modifier = Modifier.weight(1f))
+
+ Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
+ FullScreenEditorToolbarActionButton(
+ icon = Icons.AutoMirrored.Filled.Undo,
+ contentDescription = stringResource(R.string.editor_undo),
+ enabled = canUndo,
+ onClick = onUndo
+ )
+ FullScreenEditorToolbarActionButton(
+ icon = Icons.AutoMirrored.Filled.Redo,
+ contentDescription = stringResource(R.string.editor_redo),
+ enabled = canRedo,
+ onClick = onRedo
+ )
+ FullScreenEditorToolbarActionButton(
+ icon = Icons.Filled.Check,
+ contentDescription = stringResource(R.string.action_send),
+ enabled = !isOverLimit && !isSending,
+ selected = !isOverLimit && !isSending,
+ containerColor = if (isOverLimit || isSending) {
+ MaterialTheme.colorScheme.surfaceContainerHighest
+ } else {
+ MaterialTheme.colorScheme.primary
+ },
+ contentColor = if (isOverLimit || isSending) {
+ MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f)
+ } else {
+ MaterialTheme.colorScheme.onPrimary
+ },
+ onClick = onSend
+ )
+ }
+ }
+}
+
+@Composable
+internal fun FullScreenEditorTopActions(
+ isPreviewMode: Boolean,
+ parseMode: EditorParseMode,
+ showFindReplace: Boolean,
+ fontScale: Float,
+ showAiAction: Boolean,
+ onTogglePreview: () -> Unit,
+ onParseModeChange: (EditorParseMode) -> Unit,
+ onToggleFindReplace: () -> Unit,
+ onTemplatesClick: () -> Unit,
+ onAiClick: () -> Unit,
+ onZoomOut: () -> Unit,
+ onZoomIn: () -> Unit
+) {
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 8.dp, bottom = 6.dp),
+ color = MaterialTheme.colorScheme.surfaceContainer,
+ shape = RoundedCornerShape(20.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .horizontalScroll(rememberScrollState())
+ .padding(horizontal = 8.dp, vertical = 8.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ if (showAiAction) {
+ FullScreenEditorToolbarActionButton(
+ icon = Icons.Outlined.AutoAwesome,
+ contentDescription = stringResource(R.string.editor_ai),
+ onClick = onAiClick
+ )
+ }
+ FullScreenEditorToolbarActionButton(
+ icon = if (isPreviewMode) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
+ contentDescription = if (isPreviewMode) {
+ stringResource(R.string.editor_mode_edit)
+ } else {
+ stringResource(R.string.editor_mode_preview)
+ },
+ selected = isPreviewMode,
+ onClick = onTogglePreview
+ )
+ FullScreenEditorParseModeSelector(
+ parseMode = parseMode,
+ onParseModeChange = onParseModeChange
+ )
+ FullScreenEditorToolbarActionButton(
+ icon = Icons.Outlined.Search,
+ contentDescription = if (showFindReplace) {
+ stringResource(R.string.action_close)
+ } else {
+ stringResource(R.string.editor_find)
+ },
+ selected = showFindReplace,
+ onClick = onToggleFindReplace
+ )
+ FullScreenEditorToolbarActionButton(
+ icon = Icons.Outlined.Description,
+ contentDescription = stringResource(R.string.editor_templates),
+ onClick = onTemplatesClick
+ )
+ FullScreenEditorToolbarActionButton(
+ icon = Icons.Outlined.ZoomOut,
+ contentDescription = stringResource(R.string.editor_zoom_out),
+ enabled = fontScale > 0.8f,
+ onClick = onZoomOut
+ )
+ FullScreenEditorToolbarActionButton(
+ icon = Icons.Outlined.ZoomIn,
+ contentDescription = stringResource(R.string.editor_zoom_in),
+ enabled = fontScale < 1.6f,
+ onClick = onZoomIn
+ )
+ }
+ }
+}
+
+@Composable
+internal fun FullScreenEditorParseModeSelector(
+ parseMode: EditorParseMode,
+ onParseModeChange: (EditorParseMode) -> Unit
+) {
+ var expanded by remember { mutableStateOf(false) }
+ val currentLabel = when (parseMode) {
+ EditorParseMode.Plain -> stringResource(R.string.editor_parse_mode_plain)
+ EditorParseMode.Markdown -> stringResource(R.string.editor_parse_mode_markdown)
+ EditorParseMode.Html -> stringResource(R.string.editor_parse_mode_html)
+ }
+ val currentIcon = when (parseMode) {
+ EditorParseMode.Plain -> Icons.Outlined.TextFields
+ EditorParseMode.Markdown -> Icons.AutoMirrored.Outlined.Subject
+ EditorParseMode.Html -> Icons.Outlined.Code
+ }
+
+ Box {
+ Surface(
+ modifier = Modifier.height(42.dp),
+ onClick = { expanded = true },
+ shape = RoundedCornerShape(16.dp),
+ color = MaterialTheme.colorScheme.primaryContainer
+ ) {
+ Row(
+ modifier = Modifier.padding(horizontal = 12.dp),
+ horizontalArrangement = Arrangement.spacedBy(6.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = currentIcon,
+ contentDescription = null,
+ modifier = Modifier.size(18.dp),
+ tint = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ Text(
+ text = currentLabel,
+ style = MaterialTheme.typography.labelMedium,
+ color = MaterialTheme.colorScheme.onPrimaryContainer,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ Icon(
+ imageVector = Icons.Outlined.KeyboardArrowDown,
+ contentDescription = null,
+ modifier = Modifier.size(18.dp),
+ tint = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ }
+ }
+
+ DropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false },
+ shape = RoundedCornerShape(16.dp),
+ tonalElevation = 0.dp,
+ shadowElevation = 0.dp
+ ) {
+ EditorParseMode.entries.forEach { mode ->
+ val selected = mode == parseMode
+ DropdownMenuItem(
+ text = {
+ Text(
+ text = when (mode) {
+ EditorParseMode.Plain -> stringResource(R.string.editor_parse_mode_plain)
+ EditorParseMode.Markdown -> stringResource(R.string.editor_parse_mode_markdown)
+ EditorParseMode.Html -> stringResource(R.string.editor_parse_mode_html)
+ }
+ )
+ },
+ leadingIcon = {
+ Icon(
+ imageVector = when (mode) {
+ EditorParseMode.Plain -> Icons.Outlined.TextFields
+ EditorParseMode.Markdown -> Icons.AutoMirrored.Outlined.Subject
+ EditorParseMode.Html -> Icons.Outlined.Code
+ },
+ contentDescription = null
+ )
+ },
+ trailingIcon = if (selected) {
+ {
+ Icon(
+ imageVector = Icons.Filled.Check,
+ contentDescription = null
+ )
+ }
+ } else null,
+ onClick = {
+ onParseModeChange(mode)
+ expanded = false
+ }
+ )
+ }
+ }
+ }
+}
+
+@Composable
+internal fun FullScreenEditorToolbarActionButton(
+ icon: ImageVector,
+ contentDescription: String,
+ selected: Boolean = false,
+ enabled: Boolean = true,
+ containerColor: Color? = null,
+ contentColor: Color? = null,
+ onClick: () -> Unit
+) {
+ val resolvedContainerColor = containerColor ?: when {
+ !enabled -> MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = 0.55f)
+ selected -> MaterialTheme.colorScheme.primaryContainer
+ else -> MaterialTheme.colorScheme.surfaceContainerHighest
+ }
+ val resolvedContentColor = contentColor ?: when {
+ !enabled -> MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f)
+ selected -> MaterialTheme.colorScheme.onPrimaryContainer
+ else -> MaterialTheme.colorScheme.onSurfaceVariant
+ }
+
+ Surface(
+ onClick = onClick,
+ enabled = enabled,
+ shape = RoundedCornerShape(14.dp),
+ color = resolvedContainerColor
+ ) {
+ Box(
+ modifier = Modifier.size(42.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = contentDescription,
+ tint = resolvedContentColor,
+ modifier = Modifier.size(19.dp)
+ )
+ }
+ }
+}
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorControls.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorControls.kt
new file mode 100644
index 000000000..e3853b1a8
--- /dev/null
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorControls.kt
@@ -0,0 +1,419 @@
+package org.monogram.presentation.features.chats.conversation.ui.inputbar
+
+import android.widget.Toast
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.outlined.Subject
+import androidx.compose.material.icons.outlined.AlternateEmail
+import androidx.compose.material.icons.outlined.Code
+import androidx.compose.material.icons.outlined.ContentCopy
+import androidx.compose.material.icons.outlined.ContentCut
+import androidx.compose.material.icons.outlined.ContentPaste
+import androidx.compose.material.icons.outlined.FormatBold
+import androidx.compose.material.icons.outlined.FormatClear
+import androidx.compose.material.icons.outlined.FormatItalic
+import androidx.compose.material.icons.outlined.FormatStrikethrough
+import androidx.compose.material.icons.outlined.FormatUnderlined
+import androidx.compose.material.icons.outlined.Link
+import androidx.compose.material.icons.outlined.VisibilityOff
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import org.monogram.presentation.R
+
+@Composable
+internal fun FullScreenEditorMetaPill(text: String, color: Color, contentColor: Color) {
+ Surface(color = color, shape = RoundedCornerShape(999.dp)) {
+ Text(
+ text = text,
+ color = contentColor,
+ style = MaterialTheme.typography.labelMedium,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp)
+ )
+ }
+}
+
+@Composable
+internal fun FullScreenEditorToolButton(
+ icon: ImageVector,
+ hint: String,
+ enabled: Boolean = true,
+ onClick: () -> Unit
+) {
+ val context = LocalContext.current
+ Box(
+ modifier = Modifier
+ .clip(RoundedCornerShape(14.dp))
+ .combinedClickable(
+ enabled = enabled,
+ onClick = onClick,
+ onLongClick = { Toast.makeText(context, hint, Toast.LENGTH_SHORT).show() }
+ )
+ .padding(horizontal = 8.dp, vertical = 5.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ icon,
+ contentDescription = hint,
+ tint = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant.copy(
+ alpha = 0.38f
+ )
+ )
+ }
+}
+
+@Composable
+internal fun FullScreenEditorTools(
+ hasSelection: Boolean,
+ canCopy: Boolean,
+ canCut: Boolean,
+ canPaste: Boolean,
+ onCopy: () -> Unit,
+ onCut: () -> Unit,
+ onPaste: () -> Unit,
+ onBold: () -> Unit,
+ onItalic: () -> Unit,
+ onUnderline: () -> Unit,
+ onStrike: () -> Unit,
+ onSpoiler: () -> Unit,
+ onCode: () -> Unit,
+ onLink: () -> Unit,
+ onMention: () -> Unit,
+ onPre: () -> Unit,
+ onClear: () -> Unit,
+ onEmoji: () -> Unit
+) {
+ Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
+ Surface(
+ modifier = Modifier
+ .weight(1f)
+ .height(52.dp),
+ shape = RoundedCornerShape(28.dp),
+ color = MaterialTheme.colorScheme.surfaceContainerHighest
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxSize()
+ .horizontalScroll(rememberScrollState())
+ .padding(horizontal = 6.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(1.dp)
+ ) {
+ FullScreenEditorToolButton(
+ Icons.Outlined.ContentCopy,
+ stringResource(R.string.editor_action_copy),
+ canCopy,
+ onCopy
+ )
+ FullScreenEditorToolButton(
+ Icons.Outlined.ContentCut,
+ stringResource(R.string.editor_action_cut),
+ canCut,
+ onCut
+ )
+ FullScreenEditorToolButton(
+ Icons.Outlined.ContentPaste,
+ stringResource(R.string.editor_action_paste),
+ canPaste,
+ onPaste
+ )
+ FullScreenEditorToolButton(
+ Icons.Outlined.FormatBold,
+ stringResource(R.string.rich_text_bold),
+ hasSelection,
+ onBold
+ )
+ FullScreenEditorToolButton(
+ Icons.Outlined.FormatItalic,
+ stringResource(R.string.rich_text_italic),
+ hasSelection,
+ onItalic
+ )
+ FullScreenEditorToolButton(
+ Icons.Outlined.FormatUnderlined,
+ stringResource(R.string.rich_text_underline),
+ hasSelection,
+ onUnderline
+ )
+ FullScreenEditorToolButton(
+ Icons.Outlined.FormatStrikethrough,
+ stringResource(R.string.rich_text_strikethrough),
+ hasSelection,
+ onStrike
+ )
+ FullScreenEditorToolButton(
+ Icons.Outlined.VisibilityOff,
+ stringResource(R.string.rich_text_spoiler),
+ hasSelection,
+ onSpoiler
+ )
+ FullScreenEditorToolButton(
+ Icons.Outlined.Code,
+ stringResource(R.string.rich_text_code),
+ hasSelection,
+ onCode
+ )
+ FullScreenEditorToolButton(
+ Icons.Outlined.Link,
+ stringResource(R.string.rich_text_link),
+ hasSelection,
+ onLink
+ )
+ FullScreenEditorToolButton(
+ Icons.Outlined.AlternateEmail,
+ stringResource(R.string.rich_text_mention),
+ true,
+ onMention
+ )
+ FullScreenEditorToolButton(
+ Icons.AutoMirrored.Outlined.Subject,
+ stringResource(R.string.rich_text_pre),
+ hasSelection,
+ onPre
+ )
+ FullScreenEditorToolButton(
+ Icons.Outlined.FormatClear,
+ stringResource(R.string.rich_text_clear),
+ hasSelection,
+ onClear
+ )
+ }
+ }
+ Spacer(modifier = Modifier.width(8.dp))
+ Surface(
+ modifier = Modifier.size(52.dp),
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.surfaceContainerHighest
+ ) {
+ IconButton(onClick = onEmoji) {
+ Text(
+ text = "☺",
+ style = MaterialTheme.typography.headlineSmall,
+ color = MaterialTheme.colorScheme.primary
+ )
+ }
+ }
+ }
+}
+
+@Composable
+internal fun FullScreenEditorMarkupTools(
+ mode: EditorParseMode,
+ canCopy: Boolean,
+ canCut: Boolean,
+ canPaste: Boolean,
+ onCopy: () -> Unit,
+ onCut: () -> Unit,
+ onPaste: () -> Unit,
+ onBold: () -> Unit,
+ onItalic: () -> Unit,
+ onUnderline: () -> Unit,
+ onStrike: () -> Unit,
+ onSpoiler: () -> Unit,
+ onCode: () -> Unit,
+ onLink: () -> Unit,
+ onQuote: () -> Unit,
+ onPre: () -> Unit,
+ onLatex: () -> Unit,
+ onBlockLatex: () -> Unit,
+ onHeading1: () -> Unit,
+ onHeading2: () -> Unit,
+ onHeading3: () -> Unit,
+ onBulletList: () -> Unit,
+ onNumberedList: () -> Unit,
+ onDivider: () -> Unit,
+ onTable: () -> Unit
+) {
+ Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
+ Surface(
+ modifier = Modifier
+ .weight(1f)
+ .height(52.dp),
+ shape = RoundedCornerShape(28.dp),
+ color = MaterialTheme.colorScheme.surfaceContainerHighest
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxSize()
+ .horizontalScroll(rememberScrollState())
+ .padding(horizontal = 6.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(1.dp)
+ ) {
+ FullScreenEditorToolButton(
+ Icons.Outlined.ContentCopy,
+ stringResource(R.string.editor_action_copy),
+ canCopy,
+ onCopy
+ )
+ FullScreenEditorToolButton(
+ Icons.Outlined.ContentCut,
+ stringResource(R.string.editor_action_cut),
+ canCut,
+ onCut
+ )
+ FullScreenEditorToolButton(
+ Icons.Outlined.ContentPaste,
+ stringResource(R.string.editor_action_paste),
+ canPaste,
+ onPaste
+ )
+ FullScreenEditorToolButton(
+ Icons.Outlined.FormatBold,
+ stringResource(R.string.rich_text_bold),
+ true,
+ onBold
+ )
+ FullScreenEditorToolButton(
+ Icons.Outlined.FormatItalic,
+ stringResource(R.string.rich_text_italic),
+ true,
+ onItalic
+ )
+ FullScreenEditorToolButton(
+ Icons.Outlined.FormatUnderlined,
+ stringResource(R.string.rich_text_underline),
+ true,
+ onUnderline
+ )
+ FullScreenEditorToolButton(
+ Icons.Outlined.FormatStrikethrough,
+ stringResource(R.string.rich_text_strikethrough),
+ true,
+ onStrike
+ )
+ FullScreenEditorToolButton(
+ Icons.Outlined.VisibilityOff,
+ stringResource(R.string.rich_text_spoiler),
+ true,
+ onSpoiler
+ )
+ FullScreenEditorToolButton(
+ Icons.Outlined.Code,
+ stringResource(R.string.rich_text_code),
+ true,
+ onCode
+ )
+ FullScreenEditorToolButton(
+ Icons.Outlined.Link,
+ stringResource(R.string.rich_text_link),
+ true,
+ onLink
+ )
+ FullScreenEditorToolButton(
+ Icons.AutoMirrored.Outlined.Subject,
+ stringResource(R.string.rich_text_blockquote),
+ true,
+ onQuote
+ )
+ FullScreenEditorToolButton(
+ Icons.AutoMirrored.Outlined.Subject,
+ if (mode == EditorParseMode.Markdown) stringResource(R.string.rich_text_pre) else stringResource(
+ R.string.editor_html_pre
+ ),
+ true,
+ onPre
+ )
+ FullScreenEditorTokenToolButton(
+ "H1",
+ stringResource(R.string.editor_heading_1),
+ true,
+ onHeading1
+ )
+ FullScreenEditorTokenToolButton(
+ "H2",
+ stringResource(R.string.editor_heading_2),
+ true,
+ onHeading2
+ )
+ FullScreenEditorTokenToolButton(
+ "H3",
+ stringResource(R.string.editor_heading_3),
+ true,
+ onHeading3
+ )
+ FullScreenEditorTokenToolButton(
+ "UL",
+ stringResource(R.string.editor_bulleted_list),
+ true,
+ onBulletList
+ )
+ FullScreenEditorTokenToolButton(
+ "OL",
+ stringResource(R.string.editor_numbered_list),
+ true,
+ onNumberedList
+ )
+ FullScreenEditorTokenToolButton(
+ "HR",
+ stringResource(R.string.editor_divider),
+ true,
+ onDivider
+ )
+ FullScreenEditorTokenToolButton(
+ "Tbl",
+ stringResource(R.string.editor_table),
+ true,
+ onTable
+ )
+ }
+ }
+ }
+}
+
+@Composable
+internal fun FullScreenEditorTokenToolButton(
+ label: String,
+ hint: String,
+ enabled: Boolean = true,
+ onClick: () -> Unit
+) {
+ val context = LocalContext.current
+ Box(
+ modifier = Modifier
+ .clip(RoundedCornerShape(14.dp))
+ .combinedClickable(
+ enabled = enabled,
+ onClick = onClick,
+ onLongClick = { Toast.makeText(context, hint, Toast.LENGTH_SHORT).show() }
+ )
+ .padding(horizontal = 8.dp, vertical = 5.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = label,
+ style = MaterialTheme.typography.labelLarge,
+ color = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant.copy(
+ alpha = 0.38f
+ )
+ )
+ }
+}
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorFindReplaceBar.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorFindReplaceBar.kt
index 36c2596a7..c25937c82 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorFindReplaceBar.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorFindReplaceBar.kt
@@ -1,17 +1,36 @@
package org.monogram.presentation.features.chats.conversation.ui.inputbar
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.automirrored.filled.ArrowForward
+import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.Search
-import androidx.compose.material3.*
+import androidx.compose.material.icons.outlined.TextFields
+import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.monogram.presentation.R
-import org.monogram.presentation.core.ui.ItemPosition
-import org.monogram.presentation.core.ui.SettingsTextField
@Composable
fun FullScreenEditorFindReplaceBar(
@@ -30,59 +49,95 @@ fun FullScreenEditorFindReplaceBar(
Surface(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.surfaceContainer,
- shape = RoundedCornerShape(18.dp)
+ shape = RoundedCornerShape(24.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
- .padding(12.dp),
- verticalArrangement = Arrangement.spacedBy(8.dp)
+ .padding(horizontal = 12.dp, vertical = 10.dp),
+ verticalArrangement = Arrangement.spacedBy(5.dp)
) {
- SettingsTextField(
- value = query,
- onValueChange = onQueryChange,
- placeholder = stringResource(R.string.editor_find),
- icon = Icons.Outlined.Search,
- position = ItemPosition.TOP,
- singleLine = true,
- )
- SettingsTextField(
- value = replacement,
- onValueChange = onReplacementChange,
- placeholder = stringResource(R.string.editor_replace),
- icon = Icons.Outlined.Search,
- position = ItemPosition.BOTTOM,
- singleLine = true,
- )
-
- val indicatorText = if (matchesCount > 0) {
- stringResource(
- R.string.editor_matches_count,
- (currentMatchIndex + 1).coerceAtMost(matchesCount),
- matchesCount
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = stringResource(R.string.editor_find),
+ style = MaterialTheme.typography.labelLarge,
+ color = MaterialTheme.colorScheme.onSurface,
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ FindReplaceIconButton(
+ icon = Icons.Outlined.Close,
+ contentDescription = stringResource(R.string.action_close),
+ onClick = onClose
)
- } else {
- stringResource(R.string.editor_no_matches)
}
- Text(
- text = indicatorText,
- style = MaterialTheme.typography.labelMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
+ Column(verticalArrangement = Arrangement.spacedBy(1.dp)) {
+ CompactFindTextField(
+ value = query,
+ onValueChange = onQueryChange,
+ placeholder = stringResource(R.string.editor_find),
+ icon = Icons.Outlined.Search,
+ shape = RoundedCornerShape(
+ topStart = 22.dp,
+ topEnd = 22.dp,
+ bottomStart = 4.dp,
+ bottomEnd = 4.dp
+ )
+ )
+ CompactFindTextField(
+ value = replacement,
+ onValueChange = onReplacementChange,
+ placeholder = stringResource(R.string.editor_replace),
+ icon = Icons.Outlined.TextFields,
+ shape = RoundedCornerShape(
+ bottomStart = 22.dp,
+ bottomEnd = 22.dp,
+ topStart = 4.dp,
+ topEnd = 4.dp
+ )
+ )
+ }
Row(
modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
) {
- TextButton(onClick = onClose) { Text(text = stringResource(R.string.action_close)) }
- Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
- TextButton(onClick = onPrev, enabled = matchesCount > 0) {
- Text(text = stringResource(R.string.action_previous))
- }
- TextButton(onClick = onNext, enabled = matchesCount > 0) {
- Text(text = stringResource(R.string.action_next))
- }
+ Surface(
+ color = MaterialTheme.colorScheme.surfaceContainerHighest,
+ shape = RoundedCornerShape(14.dp)
+ ) {
+ Text(
+ text = if (matchesCount > 0) {
+ stringResource(
+ R.string.editor_matches_count,
+ (currentMatchIndex + 1).coerceAtMost(matchesCount),
+ matchesCount
+ )
+ } else {
+ stringResource(R.string.editor_no_matches)
+ },
+ style = MaterialTheme.typography.labelMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(horizontal = 10.dp, vertical = 7.dp)
+ )
+ }
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ FindReplaceIconButton(
+ icon = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = stringResource(R.string.action_previous),
+ enabled = matchesCount > 0,
+ onClick = onPrev
+ )
+ FindReplaceIconButton(
+ icon = Icons.AutoMirrored.Filled.ArrowForward,
+ contentDescription = stringResource(R.string.action_next),
+ enabled = matchesCount > 0,
+ onClick = onNext
+ )
}
}
@@ -93,18 +148,114 @@ fun FullScreenEditorFindReplaceBar(
Button(
onClick = onReplace,
enabled = matchesCount > 0,
- modifier = Modifier.weight(1f)
+ modifier = Modifier
+ .weight(1f)
+ .height(44.dp),
+ shape = RoundedCornerShape(14.dp)
) {
- Text(text = stringResource(R.string.editor_replace))
+ Text(
+ text = stringResource(R.string.editor_replace),
+ style = MaterialTheme.typography.labelLarge
+ )
}
Button(
onClick = onReplaceAll,
enabled = matchesCount > 0,
- modifier = Modifier.weight(1f)
+ modifier = Modifier
+ .weight(1f)
+ .height(44.dp),
+ shape = RoundedCornerShape(14.dp)
) {
- Text(text = stringResource(R.string.editor_replace_all))
+ Text(
+ text = stringResource(R.string.editor_replace_all),
+ style = MaterialTheme.typography.labelLarge
+ )
}
}
}
}
}
+
+@Composable
+private fun CompactFindTextField(
+ value: String,
+ onValueChange: (String) -> Unit,
+ placeholder: String,
+ icon: ImageVector,
+ shape: RoundedCornerShape
+) {
+ Surface(
+ modifier = Modifier.fillMaxWidth(),
+ color = MaterialTheme.colorScheme.surfaceContainerHighest,
+ shape = shape
+ ) {
+ TextField(
+ value = value,
+ onValueChange = onValueChange,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(52.dp),
+ textStyle = MaterialTheme.typography.bodyLarge,
+ placeholder = {
+ Text(
+ text = placeholder,
+ style = MaterialTheme.typography.bodyLarge
+ )
+ },
+ singleLine = true,
+ leadingIcon = {
+ Icon(
+ imageVector = icon,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ },
+ colors = TextFieldDefaults.colors(
+ focusedContainerColor = Color.Transparent,
+ unfocusedContainerColor = Color.Transparent,
+ disabledContainerColor = Color.Transparent,
+ focusedIndicatorColor = Color.Transparent,
+ unfocusedIndicatorColor = Color.Transparent,
+ disabledIndicatorColor = Color.Transparent
+ )
+ )
+ }
+}
+
+@Composable
+private fun FindReplaceIconButton(
+ icon: ImageVector,
+ contentDescription: String,
+ enabled: Boolean = true,
+ onClick: () -> Unit
+) {
+ val containerColor = if (enabled) {
+ MaterialTheme.colorScheme.surfaceContainerHighest
+ } else {
+ MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = 0.55f)
+ }
+ val contentColor = if (enabled) {
+ MaterialTheme.colorScheme.onSurfaceVariant
+ } else {
+ MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f)
+ }
+
+ Surface(
+ onClick = onClick,
+ enabled = enabled,
+ shape = RoundedCornerShape(14.dp),
+ color = containerColor
+ ) {
+ Box(
+ modifier = Modifier.size(36.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = contentDescription,
+ tint = contentColor,
+ modifier = Modifier.size(17.dp)
+ )
+ }
+ }
+}
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorMarkdownPreview.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorMarkdownPreview.kt
index 972230e42..60c6d7c85 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorMarkdownPreview.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorMarkdownPreview.kt
@@ -2,14 +2,14 @@ package org.monogram.presentation.features.chats.conversation.ui.inputbar
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ParagraphStyle
import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.TextRange
-import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.unit.sp
import org.monogram.domain.models.MessageEntityType
fun buildEditorPreviewAnnotatedString(
@@ -19,113 +19,131 @@ fun buildEditorPreviewAnnotatedString(
val builder = AnnotatedString.Builder(source.text)
source.getStringAnnotations(0, source.length)
- .filter { it.tag != RICH_ENTITY_TAG && it.tag != MENTION_TAG }
+ .filter { it.tag !in setOf(RICH_ENTITY_TAG, MENTION_TAG, LATEX_TAG) }
.forEach { builder.addStringAnnotation(it.tag, it.item, it.start, it.end) }
- source.getStringAnnotations(MENTION_TAG, 0, source.length).forEach { annotation ->
- builder.addStyle(SpanStyle(color = primaryColor), annotation.start, annotation.end)
- }
-
- Regex("@(\\w+)").findAll(source.text).forEach { match ->
- builder.addStyle(
- SpanStyle(color = primaryColor),
- match.range.first,
- match.range.last + 1
- )
- }
-
- source.getStringAnnotations(RICH_ENTITY_TAG, 0, source.length).forEach { annotation ->
- val style = decodeRichEntity(annotation.item)?.toPreviewStyle(primaryColor)
- if (style != null && annotation.start < annotation.end) {
- builder.addStyle(style, annotation.start, annotation.end)
+ extractEntities(source, emptyMap()).forEach { entity ->
+ val style = entity.type.toPreviewStyle(primaryColor) ?: return@forEach
+ val start = entity.offset.coerceIn(0, source.length)
+ val end = (entity.offset + entity.length).coerceIn(0, source.length)
+ if (start < end) {
+ builder.addStyle(style, start, end)
}
}
- return builder.toAnnotatedString()
-}
-
-fun applyMarkdownFormatting(value: TextFieldValue): TextFieldValue {
- val input = value.text
- val output = StringBuilder()
- val ranges = mutableListOf>()
- val markerStarts = mutableMapOf>()
- var i = 0
-
- while (i < input.length) {
- val linkMatch = LINK_REGEX.find(input, i)
- if (linkMatch != null && linkMatch.range.first == i) {
- val label = linkMatch.groupValues[1]
- val url = linkMatch.groupValues[2]
- val start = output.length
- output.append(label)
- val end = output.length
- if (start < end && url.isNotBlank()) {
- ranges += Triple(start, end, MessageEntityType.TextUrl(url))
- }
- i = linkMatch.range.last + 1
- continue
+ source.getStringAnnotations(LATEX_TAG, 0, source.length).forEach { annotation ->
+ if (annotation.start < annotation.end) {
+ builder.addStyle(
+ SpanStyle(
+ fontFamily = FontFamily.Monospace,
+ background = primaryColor.copy(alpha = 0.12f),
+ color = primaryColor
+ ),
+ annotation.start,
+ annotation.end
+ )
}
+ }
- val marker = MARKERS.firstOrNull { input.startsWith(it, i) }
- if (marker != null) {
- val starts = markerStarts.getOrPut(marker) { mutableListOf() }
- if (starts.isNotEmpty()) {
- val start = starts.removeAt(starts.lastIndex)
- val end = output.length
- if (start < end) {
- val type = markerToType(marker)
- if (type != null) {
- ranges += Triple(start, end, type)
- }
- }
- } else {
- starts += output.length
+ source.getStringAnnotations(EDITOR_HEADING_TAG, 0, source.length).forEach { annotation ->
+ if (annotation.start < annotation.end) {
+ val level = annotation.item.toIntOrNull()?.coerceIn(1, 3) ?: 1
+ val fontSize = when (level) {
+ 1 -> 24.sp
+ 2 -> 20.sp
+ else -> 17.sp
}
- i += marker.length
- continue
+ builder.addStyle(
+ SpanStyle(
+ fontWeight = FontWeight.Bold,
+ fontSize = fontSize,
+ color = primaryColor
+ ),
+ annotation.start,
+ annotation.end
+ )
}
-
- output.append(input[i])
- i++
}
- val annotated = buildAnnotatedString {
- append(output.toString())
- ranges.forEach { (start, end, type) ->
- val key = richEntityToAnnotation(type) ?: return@forEach
- addStringAnnotation(RICH_ENTITY_TAG, key, start, end)
+ source.getStringAnnotations(EDITOR_DIVIDER_TAG, 0, source.length).forEach { annotation ->
+ if (annotation.start < annotation.end) {
+ builder.addStyle(
+ SpanStyle(
+ color = primaryColor.copy(alpha = 0.7f)
+ ),
+ annotation.start,
+ annotation.end
+ )
+ builder.addStyle(
+ ParagraphStyle(textAlign = TextAlign.Center),
+ annotation.start,
+ annotation.end
+ )
}
}
- val newCursor = annotated.length.coerceAtLeast(0)
- return value.copy(annotatedString = annotated, selection = TextRange(newCursor))
-}
-private fun markerToType(marker: String): MessageEntityType? {
- return when (marker) {
- "**" -> MessageEntityType.Bold
- "_" -> MessageEntityType.Italic
- "~~" -> MessageEntityType.Strikethrough
- "||" -> MessageEntityType.Spoiler
- "`" -> MessageEntityType.Code
- else -> null
- }
+ return builder.toAnnotatedString()
}
private fun MessageEntityType.toPreviewStyle(primaryColor: Color): SpanStyle? {
val codeBackground = primaryColor.copy(alpha = 0.14f)
val spoilerBackground = primaryColor.copy(alpha = 0.25f)
+ val quoteBackground = primaryColor.copy(alpha = 0.09f)
+
return when (this) {
is MessageEntityType.Bold -> SpanStyle(fontWeight = FontWeight.Bold)
is MessageEntityType.Italic -> SpanStyle(fontStyle = FontStyle.Italic)
is MessageEntityType.Underline -> SpanStyle(textDecoration = TextDecoration.Underline)
is MessageEntityType.Strikethrough -> SpanStyle(textDecoration = TextDecoration.LineThrough)
is MessageEntityType.Spoiler -> SpanStyle(color = Color.Transparent, background = spoilerBackground)
+ is MessageEntityType.BlockQuote -> SpanStyle(
+ color = primaryColor,
+ background = quoteBackground,
+ fontStyle = FontStyle.Italic
+ )
+
+ is MessageEntityType.BlockQuoteExpandable -> SpanStyle(
+ color = primaryColor,
+ background = quoteBackground,
+ fontStyle = FontStyle.Italic
+ )
+
is MessageEntityType.Code -> SpanStyle(fontFamily = FontFamily.Monospace, background = codeBackground)
is MessageEntityType.Pre -> SpanStyle(fontFamily = FontFamily.Monospace, background = codeBackground)
is MessageEntityType.TextUrl -> SpanStyle(color = primaryColor, textDecoration = TextDecoration.Underline)
+ is MessageEntityType.Mention -> SpanStyle(color = primaryColor)
+ is MessageEntityType.TextMention -> SpanStyle(color = primaryColor)
+ is MessageEntityType.Hashtag -> SpanStyle(color = primaryColor)
+ is MessageEntityType.Cashtag -> SpanStyle(color = primaryColor)
+ is MessageEntityType.BotCommand -> SpanStyle(color = primaryColor)
+ is MessageEntityType.Url -> SpanStyle(
+ color = primaryColor,
+ textDecoration = TextDecoration.Underline
+ )
+
+ is MessageEntityType.Email -> SpanStyle(
+ color = primaryColor,
+ textDecoration = TextDecoration.Underline
+ )
+
+ is MessageEntityType.PhoneNumber -> SpanStyle(
+ color = primaryColor,
+ textDecoration = TextDecoration.Underline
+ )
+
+ is MessageEntityType.BankCardNumber -> SpanStyle(
+ color = primaryColor,
+ textDecoration = TextDecoration.Underline
+ )
+ is MessageEntityType.DateTime -> SpanStyle(
+ color = primaryColor,
+ textDecoration = TextDecoration.Underline
+ )
+
+ is MessageEntityType.MediaTimestamp -> SpanStyle(
+ color = primaryColor,
+ textDecoration = TextDecoration.Underline
+ )
else -> null
}
}
-
-private val MARKERS = listOf("**", "~~", "||", "`", "_")
-private val LINK_REGEX = Regex("\\[([^\\]]+)]\\(([^)]+)\\)")
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorSheet.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorSheet.kt
index fe75e973e..8acc1f342 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorSheet.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorSheet.kt
@@ -1,7 +1,6 @@
package org.monogram.presentation.features.chats.conversation.ui.inputbar
import android.content.ClipData
-import android.widget.Toast
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
@@ -11,7 +10,6 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
-import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -27,31 +25,17 @@ import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
-import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup
-import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
-import androidx.compose.material.icons.automirrored.outlined.Subject
-import androidx.compose.material.icons.filled.Check
-import androidx.compose.material.icons.outlined.AlternateEmail
import androidx.compose.material.icons.outlined.AutoAwesome
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.Code
-import androidx.compose.material.icons.outlined.ContentCopy
-import androidx.compose.material.icons.outlined.ContentCut
-import androidx.compose.material.icons.outlined.ContentPaste
-import androidx.compose.material.icons.outlined.FormatBold
-import androidx.compose.material.icons.outlined.FormatClear
-import androidx.compose.material.icons.outlined.FormatItalic
-import androidx.compose.material.icons.outlined.FormatStrikethrough
-import androidx.compose.material.icons.outlined.FormatUnderlined
import androidx.compose.material.icons.outlined.Link
import androidx.compose.material.icons.outlined.Translate
import androidx.compose.material.icons.outlined.Visibility
@@ -94,13 +78,14 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance
-import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLocale
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ParagraphStyle
import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.PlaceholderVerticalAlign
import androidx.compose.ui.text.TextRange
@@ -119,26 +104,32 @@ import androidx.core.view.WindowCompat
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.koin.compose.koinInject
+import org.monogram.domain.models.MessageEntity
import org.monogram.domain.models.MessageEntityType
import org.monogram.domain.models.StickerModel
import org.monogram.domain.repository.EditorSnippet
import org.monogram.domain.repository.EditorSnippetProvider
import org.monogram.domain.repository.FormattedTextResult
import org.monogram.domain.repository.MessageAiRepository
+import org.monogram.domain.repository.RichTextParsingRepository
import org.monogram.domain.repository.StickerRepository
import org.monogram.domain.repository.TextCompositionStyleModel
import org.monogram.presentation.R
import org.monogram.presentation.core.ui.ItemPosition
import org.monogram.presentation.core.ui.SettingsTextField
+import org.monogram.presentation.features.chats.conversation.ui.message.BigEmojiContent
+import org.monogram.presentation.features.chats.conversation.ui.message.MessageText
import org.monogram.presentation.features.chats.conversation.ui.message.addEmojiStyle
+import org.monogram.presentation.features.chats.conversation.ui.message.rememberMessageTextRenderData
import org.monogram.presentation.features.profile.logs.components.calculateDiff
import org.monogram.presentation.features.stickers.ui.menu.StickerEmojiMenu
import org.monogram.presentation.features.stickers.ui.view.StickerImage
import java.util.Locale
-private enum class AiEditorMode {
+internal enum class AiEditorMode {
Translate,
Stylize,
+ Generate,
Fix
}
@@ -209,7 +200,7 @@ private fun resolveAiStyleTitle(style: TextCompositionStyleModel): String {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun FullScreenEditorSheet(
+internal fun FullScreenEditorSheet(
visible: Boolean,
textValue: TextFieldValue,
onTextValueChange: (TextFieldValue) -> Unit,
@@ -218,14 +209,14 @@ fun FullScreenEditorSheet(
knownCustomEmojis: MutableMap,
emojiFontFamily: FontFamily,
isKeyboardVisible: Boolean,
- isOverMessageLimit: Boolean,
- currentMessageLength: Int,
maxMessageLength: Int,
+ initialParseMode: EditorParseMode,
+ sendAsRichMessage: Boolean,
stickerRepository: StickerRepository,
isPremiumUser: Boolean,
isSecretChat: Boolean,
onDismiss: () -> Unit,
- onSend: () -> Unit,
+ onSend: (TextFieldValue, EditorParseMode) -> Unit,
onEditorFocus: () -> Unit,
onDraftAutosave: (String) -> Unit = {}
) {
@@ -241,7 +232,7 @@ fun FullScreenEditorSheet(
var showLanguageDialog by rememberSaveable { mutableStateOf(false) }
var languageValue by rememberSaveable { mutableStateOf("") }
var isPreviewMode by rememberSaveable { mutableStateOf(false) }
- var markdownMode by rememberSaveable { mutableStateOf(false) }
+ var parseMode by rememberSaveable { mutableStateOf(initialParseMode) }
var showFindReplace by rememberSaveable { mutableStateOf(false) }
var findQuery by rememberSaveable { mutableStateOf("") }
var replaceValue by rememberSaveable { mutableStateOf("") }
@@ -251,6 +242,7 @@ fun FullScreenEditorSheet(
var fontScale by remember { mutableFloatStateOf(1f) }
var showAiSheet by rememberSaveable { mutableStateOf(false) }
var aiTranslateLanguage by rememberSaveable { mutableStateOf("") }
+ var aiPrompt by rememberSaveable { mutableStateOf("") }
var aiSelectedStyle by rememberSaveable { mutableStateOf("") }
var aiAddEmojis by rememberSaveable { mutableStateOf(false) }
var aiMode by rememberSaveable { mutableStateOf(AiEditorMode.Stylize) }
@@ -259,9 +251,13 @@ fun FullScreenEditorSheet(
var aiResultTextValue by remember { mutableStateOf(null) }
var aiErrorMessage by rememberSaveable { mutableStateOf(null) }
var aiLoading by remember { mutableStateOf(false) }
+ var isSending by remember { mutableStateOf(false) }
+ val previewRevealedSpoilers = remember { mutableStateListOf() }
val snippetProvider: EditorSnippetProvider = koinInject()
val messageRepository: MessageAiRepository = koinInject()
+ val richTextParsingRepository: RichTextParsingRepository = koinInject()
+ val supportsPromptBasedAi = messageRepository.supportsPromptBasedAi
val textCompositionStyles by messageRepository.textCompositionStyles.collectAsState()
val effectiveAiStyles = remember(textCompositionStyles) {
if (textCompositionStyles.isEmpty()) DEFAULT_AI_STYLES else textCompositionStyles
@@ -291,10 +287,6 @@ fun FullScreenEditorSheet(
val redoStack = remember { mutableStateListOf() }
val matches = remember(textValue.text, findQuery) { findOccurrences(textValue.text, findQuery) }
- val wordCount = remember(textValue.text) {
- Regex("\\S+").findAll(textValue.text).count()
- }
- val readingMinutes = remember(wordCount) { ((wordCount + 179) / 180).coerceAtLeast(1) }
val previewPrimaryColor = MaterialTheme.colorScheme.primary
val previewInlineContent = remember(knownCustomEmojis.size, knownCustomEmojis.hashCode()) {
@@ -311,6 +303,159 @@ fun FullScreenEditorSheet(
}.toMap()
}
+ fun previewRawOffsetToDisplayOffset(
+ rawText: String,
+ entities: List,
+ rawOffset: Int,
+ displayTextLength: Int
+ ): Int {
+ val targetOffset = rawOffset.coerceIn(0, rawText.length)
+ val emojiEntities = entities
+ .filter { it.type is MessageEntityType.CustomEmoji }
+ .sortedBy { it.offset }
+
+ var rawPosition = 0
+ var displayPosition = 0
+
+ emojiEntities.forEach { entity ->
+ val safeStart = entity.offset.coerceIn(0, rawText.length)
+ val safeEnd = (entity.offset + entity.length).coerceIn(safeStart, rawText.length)
+
+ if (targetOffset <= safeStart) {
+ return (displayPosition + (targetOffset - rawPosition)).coerceIn(
+ 0,
+ displayTextLength
+ )
+ }
+
+ if (safeStart > rawPosition) {
+ displayPosition += safeStart - rawPosition
+ rawPosition = safeStart
+ }
+
+ val inlinePlaceholderLength = "[emoji]".length
+ if (targetOffset <= safeEnd) {
+ return (displayPosition + inlinePlaceholderLength).coerceIn(0, displayTextLength)
+ }
+
+ displayPosition += inlinePlaceholderLength
+ rawPosition = safeEnd
+ }
+
+ return (displayPosition + (targetOffset - rawPosition)).coerceIn(0, displayTextLength)
+ }
+
+ fun buildMessagePreviewText(
+ renderedText: AnnotatedString,
+ source: AnnotatedString,
+ rawText: String,
+ entities: List,
+ fontSize: Float
+ ): AnnotatedString {
+ if (source.length == 0) return renderedText
+
+ return buildAnnotatedString {
+ append(renderedText)
+
+ source.getStringAnnotations(LATEX_TAG, 0, source.length).forEach { annotation ->
+ val start = previewRawOffsetToDisplayOffset(
+ rawText = rawText,
+ entities = entities,
+ rawOffset = annotation.start,
+ displayTextLength = renderedText.length
+ )
+ val end = previewRawOffsetToDisplayOffset(
+ rawText = rawText,
+ entities = entities,
+ rawOffset = annotation.end,
+ displayTextLength = renderedText.length
+ )
+ if (start >= end) return@forEach
+
+ val isBlock = annotation.item == "block"
+ addStyle(
+ androidx.compose.ui.text.SpanStyle(
+ fontFamily = FontFamily.Monospace,
+ color = previewPrimaryColor,
+ background = previewPrimaryColor.copy(alpha = if (isBlock) 0.16f else 0.12f),
+ fontSize = if (isBlock) (fontSize * 1.08f).sp else androidx.compose.ui.unit.TextUnit.Unspecified
+ ),
+ start,
+ end
+ )
+ if (isBlock) {
+ addStyle(
+ ParagraphStyle(textAlign = TextAlign.Center),
+ start,
+ end
+ )
+ }
+ }
+
+ source.getStringAnnotations(EDITOR_HEADING_TAG, 0, source.length)
+ .forEach { annotation ->
+ val start = previewRawOffsetToDisplayOffset(
+ rawText = rawText,
+ entities = entities,
+ rawOffset = annotation.start,
+ displayTextLength = renderedText.length
+ )
+ val end = previewRawOffsetToDisplayOffset(
+ rawText = rawText,
+ entities = entities,
+ rawOffset = annotation.end,
+ displayTextLength = renderedText.length
+ )
+ if (start >= end) return@forEach
+
+ val level = annotation.item.toIntOrNull()?.coerceIn(1, 3) ?: 1
+ addStyle(
+ androidx.compose.ui.text.SpanStyle(
+ fontWeight = FontWeight.Bold,
+ color = previewPrimaryColor,
+ fontSize = when (level) {
+ 1 -> (fontSize * 1.48f).sp
+ 2 -> (fontSize * 1.28f).sp
+ else -> (fontSize * 1.12f).sp
+ }
+ ),
+ start,
+ end
+ )
+ }
+
+ source.getStringAnnotations(EDITOR_DIVIDER_TAG, 0, source.length)
+ .forEach { annotation ->
+ val start = previewRawOffsetToDisplayOffset(
+ rawText = rawText,
+ entities = entities,
+ rawOffset = annotation.start,
+ displayTextLength = renderedText.length
+ )
+ val end = previewRawOffsetToDisplayOffset(
+ rawText = rawText,
+ entities = entities,
+ rawOffset = annotation.end,
+ displayTextLength = renderedText.length
+ )
+ if (start >= end) return@forEach
+
+ addStyle(
+ androidx.compose.ui.text.SpanStyle(
+ color = previewPrimaryColor.copy(alpha = 0.7f)
+ ),
+ start,
+ end
+ )
+ addStyle(
+ ParagraphStyle(textAlign = TextAlign.Center),
+ start,
+ end
+ )
+ }
+ }
+ }
+
fun buildPreviewForDisplay(source: AnnotatedString): AnnotatedString {
val previewAnnotated = buildEditorPreviewAnnotatedString(
source = source,
@@ -360,15 +505,18 @@ fun FullScreenEditorSheet(
}
}
- val previewText = remember(
- textValue.annotatedString,
- knownCustomEmojis.size,
- knownCustomEmojis.hashCode(),
- previewPrimaryColor,
- emojiFontFamily
- ) {
- buildPreviewForDisplay(textValue.annotatedString)
+ val displayTextValue = remember(textValue, parseMode) {
+ applyEditorFormatting(textValue, parseMode)
}
+ LaunchedEffect(displayTextValue.annotatedString) {
+ previewRevealedSpoilers.clear()
+ }
+ val displayMessageLength = displayTextValue.text.length
+ val isDisplayOverMessageLimit = displayMessageLength > maxMessageLength
+ val wordCount = remember(displayTextValue.text) {
+ Regex("\\S+").findAll(displayTextValue.text).count()
+ }
+ val readingMinutes = remember(wordCount) { ((wordCount + 179) / 180).coerceAtLeast(1) }
fun applyEditorChange(newValue: TextFieldValue, trackHistory: Boolean = true) {
if (newValue == textValue) return
@@ -393,8 +541,42 @@ fun FullScreenEditorSheet(
)
}
- val entities = remember(textValue.annotatedString, knownCustomEmojis.size) {
- extractEntities(textValue.annotatedString, knownCustomEmojis)
+ val entities = remember(
+ displayTextValue.annotatedString,
+ knownCustomEmojis.size,
+ knownCustomEmojis.hashCode()
+ ) {
+ extractEntities(displayTextValue.annotatedString, knownCustomEmojis)
+ }
+ val previewFontSize =
+ MaterialTheme.typography.bodyLarge.fontSize.value * fontScale.coerceIn(0.8f, 1.6f)
+ val previewCustomEmojiPaths = remember(knownCustomEmojis.size, knownCustomEmojis.hashCode()) {
+ knownCustomEmojis.mapValues { (_, sticker) -> sticker.path }
+ }
+ val previewRenderData = rememberMessageTextRenderData(
+ text = displayTextValue.text,
+ entities = entities,
+ fontSize = previewFontSize,
+ isOutgoing = false,
+ revealedSpoilers = previewRevealedSpoilers,
+ emojiFontFamily = emojiFontFamily,
+ customEmojiPaths = previewCustomEmojiPaths
+ )
+ val previewMessageText = remember(
+ previewRenderData.annotatedText,
+ displayTextValue.annotatedString,
+ displayTextValue.text,
+ entities,
+ previewFontSize,
+ previewPrimaryColor
+ ) {
+ buildMessagePreviewText(
+ renderedText = previewRenderData.annotatedText,
+ source = displayTextValue.annotatedString,
+ rawText = displayTextValue.text,
+ entities = entities,
+ fontSize = previewFontSize
+ )
}
val richEntityCount = remember(entities) { entities.count { richEntityToAnnotation(it.type) != null } }
val hasSelection = hasFormattableSelection(textValue)
@@ -415,11 +597,18 @@ fun FullScreenEditorSheet(
aiResultText = buildPreviewForDisplay(mappedTextValue.annotatedString)
}
- fun runAiRequest(request: suspend () -> FormattedTextResult?) {
- if (textValue.text.isBlank()) {
+ fun runAiRequest(
+ requireText: Boolean = true,
+ request: suspend () -> FormattedTextResult?
+ ) {
+ if (requireText && textValue.text.isBlank()) {
aiErrorMessage = context.getString(R.string.editor_ai_error_empty)
return
}
+ if (!requireText && aiPrompt.isBlank()) {
+ aiErrorMessage = context.getString(R.string.editor_ai_error_empty_prompt)
+ return
+ }
aiScope.launch {
aiLoading = true
@@ -453,6 +642,7 @@ fun FullScreenEditorSheet(
undoStack.clear()
redoStack.clear()
showAutoSaved = false
+ parseMode = initialParseMode
aiErrorMessage = null
aiShowDiffMode = true
aiResultText = null
@@ -478,12 +668,17 @@ fun FullScreenEditorSheet(
showAutoSaved = false
}
- val onSendClick: () -> Unit = {
- val outgoingValue = if (markdownMode) applyMarkdownFormatting(textValue) else textValue
- if (outgoingValue != textValue) {
- onTextValueChange(outgoingValue)
+ val onSendClick: () -> Unit = send@{
+ if (isSending) return@send
+
+ fun sendResolvedValue(outgoingValue: TextFieldValue) {
+ if (outgoingValue != textValue) {
+ onTextValueChange(outgoingValue)
+ }
+ onSend(outgoingValue, parseMode)
}
- onSend()
+
+ sendResolvedValue(textValue)
}
Dialog(
@@ -502,15 +697,12 @@ fun FullScreenEditorSheet(
.padding(16.dp)
.imePadding()
) {
- FullScreenEditorHeader(isOverMessageLimit, onDismiss, onSendClick)
- FullScreenEditorTopActions(
+ FullScreenEditorHeader(
+ isOverLimit = isDisplayOverMessageLimit,
+ isSending = isSending,
canUndo = undoStack.isNotEmpty(),
canRedo = redoStack.isNotEmpty(),
- isPreviewMode = isPreviewMode,
- markdownMode = markdownMode,
- showFindReplace = showFindReplace,
- fontScale = fontScale,
- showAiAction = canUseAi,
+ onDismiss = onDismiss,
onUndo = {
if (undoStack.isNotEmpty()) {
val previous = undoStack.removeAt(undoStack.lastIndex)
@@ -525,8 +717,16 @@ fun FullScreenEditorSheet(
onTextValueChange(next)
}
},
+ onSend = onSendClick
+ )
+ FullScreenEditorTopActions(
+ isPreviewMode = isPreviewMode,
+ parseMode = parseMode,
+ showFindReplace = showFindReplace,
+ fontScale = fontScale,
+ showAiAction = canUseAi,
onTogglePreview = { isPreviewMode = !isPreviewMode },
- onToggleMarkdown = { markdownMode = !markdownMode },
+ onParseModeChange = { parseMode = it },
onToggleFindReplace = { showFindReplace = !showFindReplace },
onTemplatesClick = { showTemplatesSheet = true },
onAiClick = {
@@ -568,6 +768,8 @@ fun FullScreenEditorSheet(
)
}
+ Spacer(modifier = Modifier.height(if (showFindReplace) 14.dp else 10.dp))
+
Row(
modifier = Modifier
.fillMaxWidth()
@@ -575,11 +777,17 @@ fun FullScreenEditorSheet(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
FullScreenEditorMetaPill(
- text = stringResource(R.string.message_length_counter, currentMessageLength, maxMessageLength),
- color = if (isOverMessageLimit) MaterialTheme.colorScheme.error.copy(alpha = 0.22f) else MaterialTheme.colorScheme.primary.copy(
+ text = stringResource(
+ R.string.message_length_counter,
+ displayMessageLength,
+ maxMessageLength
+ ),
+ color = if (isDisplayOverMessageLimit) MaterialTheme.colorScheme.error.copy(
+ alpha = 0.22f
+ ) else MaterialTheme.colorScheme.primary.copy(
alpha = 0.2f
),
- contentColor = if (isOverMessageLimit) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary
+ contentColor = if (isDisplayOverMessageLimit) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary
)
FullScreenEditorMetaPill(
text = stringResource(R.string.fullscreen_editor_blocks, richEntityCount),
@@ -606,17 +814,40 @@ fun FullScreenEditorSheet(
color = MaterialTheme.colorScheme.surfaceContainerHigh
) {
if (isPreviewMode) {
- Text(
- text = previewText,
- inlineContent = previewInlineContent,
- style = MaterialTheme.typography.bodyLarge.copy(
- fontSize = MaterialTheme.typography.bodyLarge.fontSize * fontScale.coerceIn(0.8f, 1.6f)
- ),
- color = MaterialTheme.colorScheme.onSurface,
+ val previewTextStyle = MaterialTheme.typography.bodyLarge.copy(
+ fontSize = if (previewRenderData.isBigEmoji) (previewFontSize * 5f).sp else previewFontSize.sp,
+ lineHeight = if (previewRenderData.isBigEmoji) (previewFontSize * 5.5f).sp else (previewFontSize * 1.1f).sp
+ )
+ Box(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp, vertical = 12.dp)
- )
+ .verticalScroll(rememberScrollState())
+ ) {
+ if (previewRenderData.isBigEmoji && previewRenderData.bigEmojiItems.isNotEmpty()) {
+ BigEmojiContent(
+ items = previewRenderData.bigEmojiItems,
+ sizeDp = previewFontSize * 5f,
+ emojiFontFamily = emojiFontFamily
+ )
+ } else {
+ MessageText(
+ text = previewMessageText,
+ rawText = displayTextValue.text,
+ inlineContent = previewRenderData.inlineContent,
+ entities = entities,
+ style = previewTextStyle,
+ color = MaterialTheme.colorScheme.onSurface,
+ onSpoilerClick = { spoilerKey ->
+ if (previewRevealedSpoilers.contains(spoilerKey)) {
+ previewRevealedSpoilers.remove(spoilerKey)
+ } else {
+ previewRevealedSpoilers.add(spoilerKey)
+ }
+ }
+ )
+ }
+ }
} else {
InputTextField(
textValue = textValue,
@@ -644,7 +875,7 @@ fun FullScreenEditorSheet(
}
}
Spacer(modifier = Modifier.height(10.dp))
- AnimatedVisibility(visible = !isPreviewMode) {
+ AnimatedVisibility(visible = !isPreviewMode && parseMode == EditorParseMode.Plain) {
FullScreenEditorTools(
hasSelection = hasSelection,
canCopy = hasTextSelection,
@@ -691,6 +922,245 @@ fun FullScreenEditorSheet(
onEmoji = { showEmojiPicker = true }
)
}
+ AnimatedVisibility(visible = !isPreviewMode && parseMode != EditorParseMode.Plain) {
+ FullScreenEditorMarkupTools(
+ mode = parseMode,
+ canCopy = hasTextSelection,
+ canCut = canWriteText && hasTextSelection,
+ canPaste = canPasteFromClipboard,
+ onCopy = {
+ selectedTextOrNull(textValue)?.let { selectedText ->
+ nativeClipboard.setPrimaryClip(
+ ClipData.newPlainText(
+ "",
+ selectedText
+ )
+ )
+ }
+ },
+ onCut = {
+ selectedTextOrNull(textValue)?.let { selectedText ->
+ nativeClipboard.setPrimaryClip(
+ ClipData.newPlainText(
+ "",
+ selectedText
+ )
+ )
+ applyEditorChange(replaceSelection(textValue, ""))
+ }
+ },
+ onPaste = {
+ val clipboardText =
+ nativeClipboard.primaryClip?.takeIf { it.itemCount > 0 }
+ ?.getItemAt(0)
+ ?.coerceToText(context)
+ ?.toString()
+ .orEmpty()
+ if (clipboardText.isNotEmpty()) {
+ applyEditorChange(replaceSelection(textValue, clipboardText))
+ }
+ },
+ onBold = {
+ applyEditorChange(
+ when (parseMode) {
+ EditorParseMode.Markdown -> wrapSelectionWith(
+ textValue,
+ "**",
+ "**"
+ )
+
+ EditorParseMode.Html -> wrapSelectionWith(
+ textValue,
+ "",
+ ""
+ )
+
+ EditorParseMode.Plain -> textValue
+ }
+ )
+ },
+ onItalic = {
+ applyEditorChange(
+ when (parseMode) {
+ EditorParseMode.Markdown -> wrapSelectionWith(
+ textValue,
+ "_",
+ "_"
+ )
+
+ EditorParseMode.Html -> wrapSelectionWith(
+ textValue,
+ "",
+ ""
+ )
+
+ EditorParseMode.Plain -> textValue
+ }
+ )
+ },
+ onUnderline = {
+ applyEditorChange(
+ when (parseMode) {
+ EditorParseMode.Markdown -> wrapSelectionWith(
+ textValue,
+ "__",
+ "__"
+ )
+
+ EditorParseMode.Html -> wrapSelectionWith(
+ textValue,
+ "",
+ ""
+ )
+
+ EditorParseMode.Plain -> textValue
+ }
+ )
+ },
+ onStrike = {
+ applyEditorChange(
+ when (parseMode) {
+ EditorParseMode.Markdown -> wrapSelectionWith(
+ textValue,
+ "~~",
+ "~~"
+ )
+
+ EditorParseMode.Html -> wrapSelectionWith(
+ textValue,
+ "",
+ ""
+ )
+
+ EditorParseMode.Plain -> textValue
+ }
+ )
+ },
+ onSpoiler = {
+ applyEditorChange(
+ when (parseMode) {
+ EditorParseMode.Markdown -> wrapSelectionWith(
+ textValue,
+ "||",
+ "||"
+ )
+
+ EditorParseMode.Html -> wrapSelectionWith(
+ textValue,
+ "",
+ ""
+ )
+
+ EditorParseMode.Plain -> textValue
+ }
+ )
+ },
+ onCode = {
+ applyEditorChange(
+ when (parseMode) {
+ EditorParseMode.Markdown -> wrapSelectionWith(
+ textValue,
+ "`",
+ "`"
+ )
+
+ EditorParseMode.Html -> wrapSelectionWith(
+ textValue,
+ "",
+ ""
+ )
+
+ EditorParseMode.Plain -> textValue
+ }
+ )
+ },
+ onLink = {
+ linkValue = currentTextUrl(textValue) ?: "https://"
+ showLinkDialog = true
+ },
+ onQuote = {
+ applyEditorChange(
+ when (parseMode) {
+ EditorParseMode.Markdown -> prefixSelectionLines(
+ textValue,
+ "> "
+ )
+
+ EditorParseMode.Html -> wrapSelectionWith(
+ textValue,
+ "",
+ "
"
+ )
+
+ EditorParseMode.Plain -> textValue
+ }
+ )
+ },
+ onPre = {
+ languageValue = ""
+ showLanguageDialog = true
+ },
+ onLatex = {
+ applyEditorChange(
+ when (parseMode) {
+ EditorParseMode.Markdown -> wrapSelectionWith(
+ textValue,
+ "$",
+ "$",
+ "x^2 + y^2 = z^2"
+ )
+
+ EditorParseMode.Html -> wrapSelectionWith(
+ textValue,
+ "",
+ "",
+ "x^2 + y^2 = z^2"
+ )
+
+ EditorParseMode.Plain -> textValue
+ }
+ )
+ },
+ onBlockLatex = {
+ applyEditorChange(
+ when (parseMode) {
+ EditorParseMode.Markdown -> replaceSelection(
+ textValue,
+ "\n$$\nx^2 + y^2 = z^2\n$$\n"
+ )
+
+ EditorParseMode.Html -> replaceSelection(
+ textValue,
+ "\nx^2 + y^2 = z^2\n"
+ )
+
+ EditorParseMode.Plain -> textValue
+ }
+ )
+ },
+ onHeading1 = {
+ applyEditorChange(applyMarkupHeading(textValue, parseMode, 1))
+ },
+ onHeading2 = {
+ applyEditorChange(applyMarkupHeading(textValue, parseMode, 2))
+ },
+ onHeading3 = {
+ applyEditorChange(applyMarkupHeading(textValue, parseMode, 3))
+ },
+ onBulletList = {
+ applyEditorChange(applyMarkupBulletList(textValue, parseMode))
+ },
+ onNumberedList = {
+ applyEditorChange(applyMarkupNumberedList(textValue, parseMode))
+ },
+ onDivider = {
+ applyEditorChange(applyMarkupDivider(textValue, parseMode))
+ },
+ onTable = {
+ applyEditorChange(applyMarkupTable(textValue, parseMode))
+ }
+ )
+ }
AnimatedVisibility(visible = showAutoSaved) {
Text(
text = stringResource(R.string.editor_autosave_done),
@@ -734,7 +1204,13 @@ fun FullScreenEditorSheet(
confirmButton = {
TextButton(onClick = {
normalizeEditorUrl(linkValue)?.let {
- applyEditorChange(applyTextUrlEntity(textValue, it))
+ applyEditorChange(
+ when (parseMode) {
+ EditorParseMode.Plain -> applyTextUrlEntity(textValue, it)
+ EditorParseMode.Markdown -> applyMarkdownLink(textValue, it)
+ EditorParseMode.Html -> applyHtmlLink(textValue, it)
+ }
+ )
}
showLinkDialog = false
}) { Text(text = stringResource(R.string.action_apply)) }
@@ -763,7 +1239,13 @@ fun FullScreenEditorSheet(
},
confirmButton = {
TextButton(onClick = {
- applyEditorChange(applyPreEntity(textValue, languageValue))
+ applyEditorChange(
+ when (parseMode) {
+ EditorParseMode.Plain -> applyPreEntity(textValue, languageValue)
+ EditorParseMode.Markdown -> applyMarkdownPre(textValue, languageValue)
+ EditorParseMode.Html -> applyHtmlPre(textValue, languageValue)
+ }
+ )
showLanguageDialog = false
}) { Text(text = stringResource(R.string.action_apply)) }
},
@@ -794,7 +1276,7 @@ fun FullScreenEditorSheet(
}
if (showAiSheet) {
- FullScreenEditorAiSheet(
+ FullScreenEditorAiSheetContent(
visible = showAiSheet,
mode = aiMode,
loading = aiLoading,
@@ -803,6 +1285,7 @@ fun FullScreenEditorSheet(
stickerRepository = stickerRepository,
selectedStyleName = aiSelectedStyle,
translateLanguage = aiTranslateLanguage,
+ prompt = aiPrompt,
addEmojis = aiAddEmojis,
emojiFontFamily = emojiFontFamily,
inlineContent = previewInlineContent,
@@ -810,6 +1293,7 @@ fun FullScreenEditorSheet(
resultText = aiResultText,
resultPlainText = aiResultTextValue?.text,
showDiffMode = aiShowDiffMode,
+ supportsPromptBasedAi = supportsPromptBasedAi,
onDismiss = {
showAiSheet = false
aiErrorMessage = null
@@ -839,6 +1323,13 @@ fun FullScreenEditorSheet(
aiResultText = null
aiResultTextValue = null
},
+ onPromptChange = {
+ aiPrompt = it
+ aiErrorMessage = null
+ aiShowDiffMode = true
+ aiResultText = null
+ aiResultTextValue = null
+ },
onAddEmojisChange = {
aiAddEmojis = it
aiErrorMessage = null
@@ -870,6 +1361,14 @@ fun FullScreenEditorSheet(
)
}
+ AiEditorMode.Generate -> runAiRequest(requireText = false) {
+ messageRepository.generateTextWithAi(
+ prompt = aiPrompt.trim(),
+ languageCode = aiTranslateLanguage.ifBlank { Locale.getDefault().language },
+ addEmojis = aiAddEmojis
+ )
+ }
+
AiEditorMode.Fix -> runAiRequest {
messageRepository.fixTextWithAi(
text = textValue.text,
@@ -933,114 +1432,6 @@ private fun FullScreenEditorSystemBars() {
}
}
-@Composable
-private fun FullScreenEditorHeader(isOverLimit: Boolean, onDismiss: () -> Unit, onSend: () -> Unit) {
- Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
- IconButton(onClick = onDismiss) {
- Icon(
- Icons.AutoMirrored.Filled.ArrowBack,
- contentDescription = stringResource(R.string.action_cancel),
- tint = MaterialTheme.colorScheme.primary
- )
- }
- Text(
- text = stringResource(R.string.fullscreen_editor_title),
- style = MaterialTheme.typography.headlineSmall,
- textAlign = TextAlign.Center,
- modifier = Modifier.weight(1f)
- )
- IconButton(onClick = onSend, enabled = !isOverLimit) {
- Icon(
- imageVector = Icons.Filled.Check,
- contentDescription = stringResource(R.string.action_send),
- tint = if (isOverLimit) MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f) else MaterialTheme.colorScheme.primary
- )
- }
- }
-}
-
-@Composable
-private fun FullScreenEditorTopActions(
- canUndo: Boolean,
- canRedo: Boolean,
- isPreviewMode: Boolean,
- markdownMode: Boolean,
- showFindReplace: Boolean,
- fontScale: Float,
- showAiAction: Boolean,
- onUndo: () -> Unit,
- onRedo: () -> Unit,
- onTogglePreview: () -> Unit,
- onToggleMarkdown: () -> Unit,
- onToggleFindReplace: () -> Unit,
- onTemplatesClick: () -> Unit,
- onAiClick: () -> Unit,
- onZoomOut: () -> Unit,
- onZoomIn: () -> Unit
-) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .horizontalScroll(rememberScrollState())
- .padding(bottom = 8.dp),
- horizontalArrangement = Arrangement.spacedBy(4.dp)
- ) {
- TextButton(onClick = onUndo, enabled = canUndo) {
- Text(text = stringResource(R.string.editor_undo))
- }
- TextButton(onClick = onRedo, enabled = canRedo) {
- Text(text = stringResource(R.string.editor_redo))
- }
- if (showAiAction) {
- TextButton(onClick = onAiClick) {
- Icon(
- imageVector = Icons.Outlined.AutoAwesome,
- contentDescription = null,
- modifier = Modifier.size(18.dp)
- )
- Spacer(modifier = Modifier.width(4.dp))
- Text(text = stringResource(R.string.editor_ai))
- }
- }
- TextButton(onClick = onTogglePreview) {
- Text(
- text = if (isPreviewMode) {
- stringResource(R.string.editor_mode_edit)
- } else {
- stringResource(R.string.editor_mode_preview)
- }
- )
- }
- TextButton(onClick = onToggleMarkdown) {
- Text(
- text = if (markdownMode) {
- stringResource(R.string.editor_markdown_on)
- } else {
- stringResource(R.string.editor_markdown_off)
- }
- )
- }
- TextButton(onClick = onToggleFindReplace) {
- Text(
- text = if (showFindReplace) {
- stringResource(R.string.action_close)
- } else {
- stringResource(R.string.editor_find)
- }
- )
- }
- TextButton(onClick = onTemplatesClick) {
- Text(text = stringResource(R.string.editor_templates))
- }
- TextButton(onClick = onZoomOut, enabled = fontScale > 0.8f) {
- Text(text = stringResource(R.string.editor_zoom_out))
- }
- TextButton(onClick = onZoomIn, enabled = fontScale < 1.6f) {
- Text(text = stringResource(R.string.editor_zoom_in))
- }
- }
-}
-
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun FullScreenEditorAiSheet(
@@ -1080,6 +1471,7 @@ private fun FullScreenEditorAiSheet(
val runButtonText = when (mode) {
AiEditorMode.Translate -> stringResource(R.string.editor_ai_run_translate)
AiEditorMode.Stylize -> stringResource(R.string.editor_ai_run_stylize)
+ AiEditorMode.Generate -> stringResource(R.string.editor_ai_run_generate)
AiEditorMode.Fix -> stringResource(R.string.editor_ai_run_fix)
}
val sectionTitleStyle = MaterialTheme.typography.labelLarge
@@ -1131,7 +1523,7 @@ private fun FullScreenEditorAiSheet(
}
val actionButtonText = if (resultText != null) stringResource(R.string.editor_ai_apply_result) else runButtonText
val languageOptions = remember { buildAiLanguageOptions() }
- val fallbackLanguageCode = Locale.getDefault().language.ifBlank { "en" }
+ val fallbackLanguageCode = LocalLocale.current.platformLocale.language.ifBlank { "en" }
val selectedLanguageCode = translateLanguage.ifBlank { fallbackLanguageCode }
val selectedLanguage = languageOptions.firstOrNull { it.code == selectedLanguageCode }
var languageMenuExpanded by remember { mutableStateOf(false) }
@@ -1368,6 +1760,7 @@ private fun FullScreenEditorAiSheet(
}
}
+ AiEditorMode.Generate -> Unit
AiEditorMode.Fix -> Unit
}
}
@@ -1635,268 +2028,3 @@ private fun AiStyleChip(
}
}
-@Composable
-private fun FullScreenEditorMetaPill(text: String, color: Color, contentColor: Color) {
- Surface(color = color, shape = RoundedCornerShape(999.dp)) {
- Text(
- text = text,
- color = contentColor,
- style = MaterialTheme.typography.labelMedium,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp)
- )
- }
-}
-
-@Composable
-private fun FullScreenEditorToolButton(icon: ImageVector, hint: String, enabled: Boolean = true, onClick: () -> Unit) {
- val context = LocalContext.current
- Box(
- modifier = Modifier
- .clip(RoundedCornerShape(16.dp))
- .combinedClickable(
- enabled = enabled,
- onClick = onClick,
- onLongClick = { Toast.makeText(context, hint, Toast.LENGTH_SHORT).show() })
- .padding(horizontal = 10.dp, vertical = 8.dp),
- contentAlignment = Alignment.Center
- ) {
- Icon(
- icon,
- contentDescription = hint,
- tint = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant.copy(
- alpha = 0.38f
- )
- )
- }
-}
-
-@Composable
-private fun FullScreenEditorTools(
- hasSelection: Boolean,
- canCopy: Boolean,
- canCut: Boolean,
- canPaste: Boolean,
- onCopy: () -> Unit,
- onCut: () -> Unit,
- onPaste: () -> Unit,
- onBold: () -> Unit,
- onItalic: () -> Unit,
- onUnderline: () -> Unit,
- onStrike: () -> Unit,
- onSpoiler: () -> Unit,
- onCode: () -> Unit,
- onLink: () -> Unit,
- onMention: () -> Unit,
- onPre: () -> Unit,
- onClear: () -> Unit,
- onEmoji: () -> Unit
-) {
- Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
- Surface(
- modifier = Modifier
- .weight(1f)
- .height(58.dp),
- shape = RoundedCornerShape(32.dp),
- color = MaterialTheme.colorScheme.surfaceContainerHighest
- ) {
- Row(
- modifier = Modifier
- .fillMaxSize()
- .horizontalScroll(rememberScrollState())
- .padding(horizontal = 8.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(2.dp)
- ) {
- FullScreenEditorToolButton(
- Icons.Outlined.ContentCopy,
- stringResource(R.string.editor_action_copy),
- canCopy,
- onCopy
- )
- FullScreenEditorToolButton(
- Icons.Outlined.ContentCut,
- stringResource(R.string.editor_action_cut),
- canCut,
- onCut
- )
- FullScreenEditorToolButton(
- Icons.Outlined.ContentPaste,
- stringResource(R.string.editor_action_paste),
- canPaste,
- onPaste
- )
- FullScreenEditorToolButton(
- Icons.Outlined.FormatBold,
- stringResource(R.string.rich_text_bold),
- hasSelection,
- onBold
- )
- FullScreenEditorToolButton(
- Icons.Outlined.FormatItalic,
- stringResource(R.string.rich_text_italic),
- hasSelection,
- onItalic
- )
- FullScreenEditorToolButton(
- Icons.Outlined.FormatUnderlined,
- stringResource(R.string.rich_text_underline),
- hasSelection,
- onUnderline
- )
- FullScreenEditorToolButton(
- Icons.Outlined.FormatStrikethrough,
- stringResource(R.string.rich_text_strikethrough),
- hasSelection,
- onStrike
- )
- FullScreenEditorToolButton(
- Icons.Outlined.VisibilityOff,
- stringResource(R.string.rich_text_spoiler),
- hasSelection,
- onSpoiler
- )
- FullScreenEditorToolButton(
- Icons.Outlined.Code,
- stringResource(R.string.rich_text_code),
- hasSelection,
- onCode
- )
- FullScreenEditorToolButton(
- Icons.Outlined.Link,
- stringResource(R.string.rich_text_link),
- hasSelection,
- onLink
- )
- FullScreenEditorToolButton(
- Icons.Outlined.AlternateEmail,
- stringResource(R.string.rich_text_mention),
- true,
- onMention
- )
- FullScreenEditorToolButton(
- Icons.AutoMirrored.Outlined.Subject,
- stringResource(R.string.rich_text_pre),
- hasSelection,
- onPre
- )
- FullScreenEditorToolButton(
- Icons.Outlined.FormatClear,
- stringResource(R.string.rich_text_clear),
- hasSelection,
- onClear
- )
- }
- }
- Spacer(modifier = Modifier.width(10.dp))
- Surface(
- modifier = Modifier.size(58.dp),
- shape = CircleShape,
- color = MaterialTheme.colorScheme.surfaceContainerHighest
- ) {
- IconButton(onClick = onEmoji) {
- Text(
- text = "☺",
- style = MaterialTheme.typography.headlineSmall,
- color = MaterialTheme.colorScheme.primary
- )
- }
- }
- }
-}
-
-private fun currentTextUrl(value: TextFieldValue): String? {
- val range = normalizedSelection(value.selection) ?: return null
- return value.annotatedString.getStringAnnotations(RICH_ENTITY_TAG, range.start, range.end)
- .firstOrNull { decodeRichEntity(it.item) is MessageEntityType.TextUrl }
- ?.let { decodeRichEntity(it.item) as? MessageEntityType.TextUrl }
- ?.url
-}
-
-private fun currentPreLanguage(value: TextFieldValue): String {
- val range = normalizedSelection(value.selection) ?: return ""
- return value.annotatedString.getStringAnnotations(RICH_ENTITY_TAG, range.start, range.end)
- .firstOrNull { decodeRichEntity(it.item) is MessageEntityType.Pre }
- ?.let { decodeRichEntity(it.item) as? MessageEntityType.Pre }
- ?.language
- .orEmpty()
-}
-
-private fun selectedTextOrNull(value: TextFieldValue): String? {
- val selection = normalizedSelection(value.selection) ?: return null
- return value.text.substring(selection.start, selection.end)
-}
-
-private fun replaceSelection(value: TextFieldValue, replacement: String): TextFieldValue {
- val rawSelection = if (value.selection.start <= value.selection.end) {
- value.selection
- } else {
- TextRange(value.selection.end, value.selection.start)
- }
- val maxLength = value.annotatedString.length
- val selection = TextRange(
- start = rawSelection.start.coerceIn(0, maxLength),
- end = rawSelection.end.coerceIn(0, maxLength)
- )
- val newAnnotated = buildAnnotatedString {
- append(value.annotatedString.subSequence(0, selection.start))
- append(replacement)
- append(value.annotatedString.subSequence(selection.end, value.annotatedString.length))
- }
- val cursor = selection.start + replacement.length
- return value.copy(annotatedString = newAnnotated, selection = TextRange(cursor, cursor))
-}
-
-private fun insertSnippetAtSelection(value: TextFieldValue, snippet: String): TextFieldValue {
- if (snippet.isBlank()) return value
- val rawSelection = if (value.selection.start <= value.selection.end) {
- value.selection
- } else {
- TextRange(value.selection.end, value.selection.start)
- }
- val maxLength = value.annotatedString.length
- val selection = TextRange(
- start = rawSelection.start.coerceIn(0, maxLength),
- end = rawSelection.end.coerceIn(0, maxLength)
- )
- val newAnnotated = buildAnnotatedString {
- append(value.annotatedString.subSequence(0, selection.start))
- append(snippet)
- append(value.annotatedString.subSequence(selection.end, value.annotatedString.length))
- }
- val cursor = selection.start + snippet.length
- return value.copy(annotatedString = newAnnotated, selection = TextRange(cursor, cursor))
-}
-
-private fun normalizedSelection(selection: TextRange): TextRange? {
- if (selection.start == selection.end) return null
- return if (selection.start <= selection.end) selection else TextRange(selection.end, selection.start)
-}
-
-private fun normalizeEditorUrl(raw: String): String? {
- val trimmed = raw.trim()
- if (trimmed.isEmpty()) return null
- return if (trimmed.contains("://")) trimmed else "https://$trimmed"
-}
-
-private fun insertMentionAtSelection(value: TextFieldValue): TextFieldValue {
- val rawSelection = if (value.selection.start <= value.selection.end) value.selection else TextRange(
- value.selection.end,
- value.selection.start
- )
- val maxLength = value.annotatedString.length
- val selection = TextRange(
- start = rawSelection.start.coerceIn(0, maxLength),
- end = rawSelection.end.coerceIn(0, maxLength)
- )
- val insertion =
- if (selection.start == selection.end) "@" else "@${value.text.substring(selection.start, selection.end)}"
- val newAnnotated = buildAnnotatedString {
- append(value.annotatedString.subSequence(0, selection.start))
- append(insertion)
- append(value.annotatedString.subSequence(selection.end, value.annotatedString.length))
- }
- val newCursor = selection.start + insertion.length
- return value.copy(annotatedString = newAnnotated, selection = TextRange(newCursor, newCursor))
-}
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorTemplatesSheet.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorTemplatesSheet.kt
index 688a2a16c..9e2b187dd 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorTemplatesSheet.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorTemplatesSheet.kt
@@ -1,14 +1,44 @@
package org.monogram.presentation.features.chats.conversation.ui.inputbar
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Close
+import androidx.compose.material.icons.outlined.ContentCopy
import androidx.compose.material.icons.outlined.Edit
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
+import androidx.compose.material3.BottomSheetDefaults
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FilledTonalButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
@@ -30,10 +60,14 @@ fun FullScreenEditorTemplatesSheet(
onDeleteSnippet: (EditorSnippet) -> Unit
) {
if (!visible) return
+
var customTitle by rememberSaveable { mutableStateOf("") }
ModalBottomSheet(
onDismissRequest = onDismiss,
+ dragHandle = { BottomSheetDefaults.DragHandle() },
+ containerColor = MaterialTheme.colorScheme.background,
+ contentColor = MaterialTheme.colorScheme.onSurface,
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp)
) {
@@ -41,92 +75,159 @@ fun FullScreenEditorTemplatesSheet(
modifier = Modifier
.fillMaxWidth()
.navigationBarsPadding()
- .padding(horizontal = 16.dp, vertical = 8.dp),
- verticalArrangement = Arrangement.spacedBy(10.dp)
+ .padding(horizontal = 16.dp, vertical = 10.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
) {
- Text(
- text = stringResource(R.string.editor_templates),
- style = MaterialTheme.typography.titleMedium
- )
-
- SettingsTextField(
- value = customTitle,
- onValueChange = { customTitle = it },
- placeholder = stringResource(R.string.editor_snippet_title_hint),
- icon = Icons.Outlined.Edit,
- position = ItemPosition.STANDALONE,
- singleLine = true,
- )
+ Surface(
+ modifier = Modifier.fillMaxWidth(),
+ color = MaterialTheme.colorScheme.surfaceContainer,
+ shape = RoundedCornerShape(28.dp)
+ ) {
+ Row(
+ modifier = Modifier.padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Surface(
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.primaryContainer
+ ) {
+ Box(
+ modifier = Modifier.size(40.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.Edit,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ }
+ }
- Button(
- onClick = {
- val generated = customTitle.ifBlank {
- currentText
- .lineSequence()
- .firstOrNull { it.isNotBlank() }
- ?.take(32)
- .orEmpty()
+ Column(
+ modifier = Modifier
+ .weight(1f)
+ .padding(start = 12.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.editor_templates),
+ style = MaterialTheme.typography.titleMedium
+ )
+ Text(
+ text = stringResource(
+ R.string.scheduled_messages_summary_count,
+ snippets.size
+ ),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
}
- if (currentText.isNotBlank() && generated.isNotBlank()) {
- onSaveCurrentAsSnippet(generated)
- customTitle = ""
+
+ IconButton(onClick = onDismiss) {
+ Icon(
+ imageVector = Icons.Outlined.Close,
+ contentDescription = stringResource(R.string.action_close)
+ )
}
- },
- enabled = currentText.isNotBlank(),
- modifier = Modifier.fillMaxWidth()
+ }
+ }
+
+ Surface(
+ modifier = Modifier.fillMaxWidth(),
+ color = MaterialTheme.colorScheme.surfaceContainer,
+ shape = RoundedCornerShape(24.dp)
) {
- Text(text = stringResource(R.string.editor_save_as_snippet))
+ Column(
+ modifier = Modifier.padding(12.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.editor_save_as_snippet),
+ style = MaterialTheme.typography.labelLarge,
+ color = MaterialTheme.colorScheme.primary
+ )
+
+ SettingsTextField(
+ value = customTitle,
+ onValueChange = { customTitle = it },
+ placeholder = stringResource(R.string.editor_snippet_title_hint),
+ icon = Icons.Outlined.Edit,
+ position = ItemPosition.STANDALONE,
+ singleLine = true,
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
+ )
+
+ Button(
+ onClick = {
+ val generated = customTitle.ifBlank {
+ currentText
+ .lineSequence()
+ .firstOrNull { it.isNotBlank() }
+ ?.take(32)
+ .orEmpty()
+ }
+ if (currentText.isNotBlank() && generated.isNotBlank()) {
+ onSaveCurrentAsSnippet(generated)
+ customTitle = ""
+ }
+ },
+ enabled = currentText.isNotBlank(),
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(52.dp),
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.ContentCopy,
+ contentDescription = null
+ )
+ Spacer(modifier = Modifier.size(8.dp))
+ Text(text = stringResource(R.string.editor_save_as_snippet))
+ }
+ }
}
if (snippets.isEmpty()) {
Surface(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.surfaceContainer,
- shape = RoundedCornerShape(14.dp)
+ shape = RoundedCornerShape(24.dp)
) {
- Text(
- text = stringResource(R.string.editor_no_snippets),
- modifier = Modifier.padding(12.dp),
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
+ Column(
+ modifier = Modifier.padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(6.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.editor_no_snippets),
+ style = MaterialTheme.typography.titleSmall
+ )
+ Text(
+ text = stringResource(R.string.editor_snippet_title_hint),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
}
} else {
- LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
- items(
- items = snippets,
- key = { snippet -> "${snippet.title}|${snippet.text}" }
- ) { snippet ->
- Surface(
- modifier = Modifier.fillMaxWidth(),
- color = MaterialTheme.colorScheme.surfaceContainer,
- shape = RoundedCornerShape(14.dp)
- ) {
- Column(modifier = Modifier
- .fillMaxWidth()
- .padding(12.dp)) {
- Text(text = snippet.title, style = MaterialTheme.typography.titleSmall)
- Text(
- text = snippet.text,
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- maxLines = 2,
- overflow = TextOverflow.Ellipsis,
- modifier = Modifier.padding(top = 2.dp)
- )
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 6.dp),
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- TextButton(onClick = { onInsertSnippet(snippet.text) }) {
- Text(text = stringResource(R.string.action_insert))
- }
- TextButton(onClick = { onDeleteSnippet(snippet) }) {
- Text(text = stringResource(R.string.action_delete_message))
- }
- }
- }
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .heightIn(max = 380.dp),
+ color = MaterialTheme.colorScheme.surfaceContainer,
+ shape = RoundedCornerShape(24.dp)
+ ) {
+ LazyColumn(
+ contentPadding = PaddingValues(12.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ items(
+ items = snippets,
+ key = { snippet -> "${snippet.title}|${snippet.text}" }
+ ) { snippet ->
+ TemplateSnippetCard(
+ snippet = snippet,
+ onInsertSnippet = onInsertSnippet,
+ onDeleteSnippet = onDeleteSnippet
+ )
}
}
}
@@ -134,3 +235,84 @@ fun FullScreenEditorTemplatesSheet(
}
}
}
+
+@Composable
+private fun TemplateSnippetCard(
+ snippet: EditorSnippet,
+ onInsertSnippet: (String) -> Unit,
+ onDeleteSnippet: (EditorSnippet) -> Unit
+) {
+ Surface(
+ modifier = Modifier.fillMaxWidth(),
+ color = MaterialTheme.colorScheme.surfaceContainerHigh,
+ shape = RoundedCornerShape(20.dp)
+ ) {
+ Column(
+ modifier = Modifier.padding(14.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Surface(
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.primaryContainer
+ ) {
+ Box(
+ modifier = Modifier.size(32.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.ContentCopy,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onPrimaryContainer,
+ modifier = Modifier.size(18.dp)
+ )
+ }
+ }
+
+ Column(
+ modifier = Modifier
+ .weight(1f)
+ .padding(start = 10.dp)
+ ) {
+ Text(
+ text = snippet.title,
+ style = MaterialTheme.typography.titleSmall,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ Text(
+ text = snippet.text,
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ }
+
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ FilledTonalButton(
+ onClick = { onInsertSnippet(snippet.text) },
+ modifier = Modifier
+ .weight(1f)
+ .height(40.dp),
+ shape = RoundedCornerShape(14.dp)
+ ) {
+ Text(text = stringResource(R.string.action_insert))
+ }
+ TextButton(
+ onClick = { onDeleteSnippet(snippet) },
+ modifier = Modifier
+ .weight(1f)
+ .height(40.dp),
+ colors = ButtonDefaults.textButtonColors(
+ contentColor = MaterialTheme.colorScheme.error
+ ),
+ shape = RoundedCornerShape(14.dp)
+ ) {
+ Text(text = stringResource(R.string.action_delete_message))
+ }
+ }
+ }
+ }
+}
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorTransforms.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorTransforms.kt
new file mode 100644
index 000000000..536bb2b58
--- /dev/null
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorTransforms.kt
@@ -0,0 +1,287 @@
+package org.monogram.presentation.features.chats.conversation.ui.inputbar
+
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.input.TextFieldValue
+import org.monogram.domain.models.MessageEntityType
+
+internal fun currentTextUrl(value: TextFieldValue): String? {
+ val range = normalizedSelection(value.selection) ?: return null
+ return value.annotatedString.getStringAnnotations(RICH_ENTITY_TAG, range.start, range.end)
+ .firstOrNull { decodeRichEntity(it.item) is MessageEntityType.TextUrl }
+ ?.let { decodeRichEntity(it.item) as? MessageEntityType.TextUrl }
+ ?.url
+}
+
+internal fun currentPreLanguage(value: TextFieldValue): String {
+ val range = normalizedSelection(value.selection) ?: return ""
+ return value.annotatedString.getStringAnnotations(RICH_ENTITY_TAG, range.start, range.end)
+ .firstOrNull { decodeRichEntity(it.item) is MessageEntityType.Pre }
+ ?.let { decodeRichEntity(it.item) as? MessageEntityType.Pre }
+ ?.language
+ .orEmpty()
+}
+
+internal fun selectedTextOrNull(value: TextFieldValue): String? {
+ val selection = normalizedSelection(value.selection) ?: return null
+ return value.text.substring(selection.start, selection.end)
+}
+
+internal fun replaceSelection(value: TextFieldValue, replacement: String): TextFieldValue {
+ val rawSelection = if (value.selection.start <= value.selection.end) {
+ value.selection
+ } else {
+ TextRange(value.selection.end, value.selection.start)
+ }
+ val maxLength = value.annotatedString.length
+ val selection = TextRange(
+ start = rawSelection.start.coerceIn(0, maxLength),
+ end = rawSelection.end.coerceIn(0, maxLength)
+ )
+ val newAnnotated = buildAnnotatedString {
+ append(value.annotatedString.subSequence(0, selection.start))
+ append(replacement)
+ append(value.annotatedString.subSequence(selection.end, value.annotatedString.length))
+ }
+ val cursor = selection.start + replacement.length
+ return value.copy(annotatedString = newAnnotated, selection = TextRange(cursor, cursor))
+}
+
+internal fun insertSnippetAtSelection(value: TextFieldValue, snippet: String): TextFieldValue {
+ if (snippet.isBlank()) return value
+ val rawSelection = if (value.selection.start <= value.selection.end) {
+ value.selection
+ } else {
+ TextRange(value.selection.end, value.selection.start)
+ }
+ val maxLength = value.annotatedString.length
+ val selection = TextRange(
+ start = rawSelection.start.coerceIn(0, maxLength),
+ end = rawSelection.end.coerceIn(0, maxLength)
+ )
+ val newAnnotated = buildAnnotatedString {
+ append(value.annotatedString.subSequence(0, selection.start))
+ append(snippet)
+ append(value.annotatedString.subSequence(selection.end, value.annotatedString.length))
+ }
+ val cursor = selection.start + snippet.length
+ return value.copy(annotatedString = newAnnotated, selection = TextRange(cursor, cursor))
+}
+
+internal fun normalizedSelection(selection: TextRange): TextRange? {
+ if (selection.start == selection.end) return null
+ return if (selection.start <= selection.end) selection else TextRange(
+ selection.end,
+ selection.start
+ )
+}
+
+internal fun normalizeEditorUrl(raw: String): String? {
+ val trimmed = raw.trim()
+ if (trimmed.isEmpty()) return null
+ return if (trimmed.contains("://")) trimmed else "https://$trimmed"
+}
+
+internal fun insertMentionAtSelection(value: TextFieldValue): TextFieldValue {
+ val rawSelection =
+ if (value.selection.start <= value.selection.end) value.selection else TextRange(
+ value.selection.end,
+ value.selection.start
+ )
+ val maxLength = value.annotatedString.length
+ val selection = TextRange(
+ start = rawSelection.start.coerceIn(0, maxLength),
+ end = rawSelection.end.coerceIn(0, maxLength)
+ )
+ val insertion =
+ if (selection.start == selection.end) "@" else "@${
+ value.text.substring(
+ selection.start,
+ selection.end
+ )
+ }"
+ val newAnnotated = buildAnnotatedString {
+ append(value.annotatedString.subSequence(0, selection.start))
+ append(insertion)
+ append(value.annotatedString.subSequence(selection.end, value.annotatedString.length))
+ }
+ val newCursor = selection.start + insertion.length
+ return value.copy(annotatedString = newAnnotated, selection = TextRange(newCursor, newCursor))
+}
+
+internal fun wrapSelectionWith(
+ value: TextFieldValue,
+ prefix: String,
+ suffix: String,
+ placeholder: String = "text"
+): TextFieldValue {
+ val rawSelection =
+ if (value.selection.start <= value.selection.end) value.selection else TextRange(
+ value.selection.end,
+ value.selection.start
+ )
+ val maxLength = value.annotatedString.length
+ val selection = TextRange(
+ start = rawSelection.start.coerceIn(0, maxLength),
+ end = rawSelection.end.coerceIn(0, maxLength)
+ )
+ val selected = value.text.substring(selection.start, selection.end)
+ val content = selected.ifEmpty { placeholder }
+ val replacement = prefix + content + suffix
+ val newAnnotated = buildAnnotatedString {
+ append(value.annotatedString.subSequence(0, selection.start))
+ append(replacement)
+ append(value.annotatedString.subSequence(selection.end, value.annotatedString.length))
+ }
+ val contentStart = selection.start + prefix.length
+ val contentEnd = contentStart + content.length
+ return value.copy(
+ annotatedString = newAnnotated,
+ selection = TextRange(contentStart, contentEnd)
+ )
+}
+
+internal fun prefixSelectionLines(value: TextFieldValue, prefix: String): TextFieldValue {
+ val rawSelection =
+ if (value.selection.start <= value.selection.end) value.selection else TextRange(
+ value.selection.end,
+ value.selection.start
+ )
+ val maxLength = value.annotatedString.length
+ val selection = TextRange(
+ start = rawSelection.start.coerceIn(0, maxLength),
+ end = rawSelection.end.coerceIn(0, maxLength)
+ )
+ val selected = value.text.substring(selection.start, selection.end).ifEmpty { "quote" }
+ val replacement = selected
+ .split('\n')
+ .joinToString("\n") { line -> if (line.startsWith(prefix)) line else prefix + line }
+ val newAnnotated = buildAnnotatedString {
+ append(value.annotatedString.subSequence(0, selection.start))
+ append(replacement)
+ append(value.annotatedString.subSequence(selection.end, value.annotatedString.length))
+ }
+ return value.copy(
+ annotatedString = newAnnotated,
+ selection = TextRange(selection.start, selection.start + replacement.length)
+ )
+}
+
+internal fun applyMarkdownLink(value: TextFieldValue, url: String): TextFieldValue {
+ val label = selectedTextOrNull(value).orEmpty().ifBlank { "link" }
+ return replaceSelection(value, "[$label]($url)")
+}
+
+internal fun applyHtmlLink(value: TextFieldValue, url: String): TextFieldValue {
+ val label = selectedTextOrNull(value).orEmpty().ifBlank { "link" }
+ return replaceSelection(value, "$label")
+}
+
+internal fun applyMarkdownPre(value: TextFieldValue, language: String): TextFieldValue {
+ val selected = selectedTextOrNull(value).orEmpty().ifBlank { "code" }
+ val info = language.trim()
+ val prefix = if (info.isBlank()) "```\n" else "```$info\n"
+ return replaceSelection(value, prefix + selected + "\n```")
+}
+
+internal fun applyHtmlPre(value: TextFieldValue, language: String): TextFieldValue {
+ val selected = selectedTextOrNull(value).orEmpty().ifBlank { "code" }
+ val info = language.trim()
+ val openTag = if (info.isBlank()) "" else ""
+ return replaceSelection(value, openTag + selected + "
")
+}
+
+internal fun applyMarkupHeading(
+ value: TextFieldValue,
+ mode: EditorParseMode,
+ level: Int
+): TextFieldValue {
+ val safeLevel = level.coerceIn(1, 6)
+ return when (mode) {
+ EditorParseMode.Markdown -> prefixSelectionLines(value, "#".repeat(safeLevel) + " ")
+ EditorParseMode.Html -> wrapSelectionWith(
+ value,
+ "",
+ "",
+ "Heading $safeLevel"
+ )
+
+ EditorParseMode.Plain -> value
+ }
+}
+
+internal fun applyMarkupBulletList(value: TextFieldValue, mode: EditorParseMode): TextFieldValue {
+ return when (mode) {
+ EditorParseMode.Markdown -> prefixSelectionLines(value, "- ")
+ EditorParseMode.Html -> applyHtmlList(value, ordered = false)
+ EditorParseMode.Plain -> value
+ }
+}
+
+internal fun applyMarkupNumberedList(value: TextFieldValue, mode: EditorParseMode): TextFieldValue {
+ return when (mode) {
+ EditorParseMode.Markdown -> applyMarkdownOrderedList(value)
+ EditorParseMode.Html -> applyHtmlList(value, ordered = true)
+ EditorParseMode.Plain -> value
+ }
+}
+
+internal fun applyMarkupDivider(value: TextFieldValue, mode: EditorParseMode): TextFieldValue {
+ return when (mode) {
+ EditorParseMode.Markdown -> replaceSelection(value, "\n---\n")
+ EditorParseMode.Html -> replaceSelection(value, "\n
\n")
+ EditorParseMode.Plain -> value
+ }
+}
+
+internal fun applyMarkupTable(value: TextFieldValue, mode: EditorParseMode): TextFieldValue {
+ return when (mode) {
+ EditorParseMode.Markdown -> replaceSelection(
+ value,
+ "| Column 1 | Column 2 |\n| --- | --- |\n| Value 1 | Value 2 |"
+ )
+
+ EditorParseMode.Html -> replaceSelection(
+ value,
+ "
\n| Column 1 | Column 2 |
\n| Value 1 | Value 2 |
\n
"
+ )
+
+ EditorParseMode.Plain -> value
+ }
+}
+
+internal fun applyMarkdownOrderedList(value: TextFieldValue): TextFieldValue {
+ val rawSelection =
+ if (value.selection.start <= value.selection.end) value.selection else TextRange(
+ value.selection.end,
+ value.selection.start
+ )
+ val maxLength = value.annotatedString.length
+ val selection = TextRange(
+ start = rawSelection.start.coerceIn(0, maxLength),
+ end = rawSelection.end.coerceIn(0, maxLength)
+ )
+ val selected = value.text.substring(selection.start, selection.end).ifEmpty { "item" }
+ val replacement = selected
+ .split('\n')
+ .mapIndexed { index, line -> "${index + 1}. $line" }
+ .joinToString("\n")
+ val newAnnotated = buildAnnotatedString {
+ append(value.annotatedString.subSequence(0, selection.start))
+ append(replacement)
+ append(value.annotatedString.subSequence(selection.end, value.annotatedString.length))
+ }
+ return value.copy(
+ annotatedString = newAnnotated,
+ selection = TextRange(selection.start, selection.start + replacement.length)
+ )
+}
+
+internal fun applyHtmlList(value: TextFieldValue, ordered: Boolean): TextFieldValue {
+ val tag = if (ordered) "ol" else "ul"
+ val selected = selectedTextOrNull(value).orEmpty().ifBlank { "item" }
+ val items = selected
+ .split('\n')
+ .joinToString("\n") { line -> "${line.ifBlank { "item" }}" }
+ return replaceSelection(value, "<$tag>\n$items\n$tag>")
+}
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InputTextField.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InputTextField.kt
index 58d4cfb03..07d97215b 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InputTextField.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InputTextField.kt
@@ -82,9 +82,21 @@ private const val ENTITY_ITALIC = "italic"
private const val ENTITY_UNDERLINE = "underline"
private const val ENTITY_STRIKE = "strikethrough"
private const val ENTITY_SPOILER = "spoiler"
+private const val ENTITY_BLOCK_QUOTE = "blockquote"
+private const val ENTITY_BLOCK_QUOTE_EXPANDABLE = "blockquote_expandable"
private const val ENTITY_CODE = "code"
private const val ENTITY_PRE = "pre"
private const val ENTITY_TEXT_URL = "text_url"
+private const val ENTITY_MENTION = "mention"
+private const val ENTITY_HASHTAG = "hashtag"
+private const val ENTITY_CASHTAG = "cashtag"
+private const val ENTITY_BOT_COMMAND = "bot_command"
+private const val ENTITY_URL = "url"
+private const val ENTITY_EMAIL = "email"
+private const val ENTITY_PHONE_NUMBER = "phone_number"
+private const val ENTITY_BANK_CARD_NUMBER = "bank_card_number"
+private const val ENTITY_DATE_TIME = "date_time"
+private const val ENTITY_MEDIA_TIMESTAMP = "media_timestamp"
private object RichMenuActionBold
private object RichMenuActionItalic
@@ -179,30 +191,16 @@ fun InputTextField(
finalBuilder.addEmojiStyle(result.text, emojiFontFamily)
- mentionAnnotations.forEach { annotation ->
- if (annotation.start < annotation.end && annotation.start >= 0 && annotation.end <= result.length) {
+ val styledEntities = extractEntities(text, knownCustomEmojis)
+ styledEntities.forEach { entity ->
+ val style = entity.type.toEditorStyle(primaryColor) ?: return@forEach
+ val start = entity.offset.coerceIn(0, result.length)
+ val end = (entity.offset + entity.length).coerceIn(0, result.length)
+ if (start < end) {
finalBuilder.addStyle(
- SpanStyle(color = primaryColor),
- annotation.start,
- annotation.end
- )
- }
- }
-
- text.getStringAnnotations(RICH_ENTITY_TAG, 0, text.length).forEach { annotation ->
- val style = decodeRichEntity(annotation.item)?.toEditorStyle(primaryColor)
- if (style != null && annotation.start < annotation.end && annotation.end <= result.length) {
- finalBuilder.addStyle(style, annotation.start, annotation.end)
- }
- }
-
- val mentionRegex = Regex("@(\\w+)")
- mentionRegex.findAll(result.text).forEach { match ->
- if (mentionAnnotations.none { it.start <= match.range.first && it.end >= match.range.last + 1 }) {
- finalBuilder.addStyle(
- SpanStyle(color = primaryColor),
- match.range.first,
- match.range.last + 1
+ style,
+ start,
+ end
)
}
}
@@ -311,7 +309,7 @@ fun InputTextField(
Box(modifier = modifier.fillMaxWidth()) {
Box(
modifier = Modifier
- .padding(vertical = 10.dp)
+ .padding(vertical = 8.dp)
.heightIn(max = maxEditorHeight)
.verticalScroll(scrollState),
contentAlignment = Alignment.CenterStart
@@ -801,19 +799,33 @@ fun extractEntities(text: AnnotatedString, knownCustomEmojis: Map
- if (entities.none { it.offset <= match.range.first && it.offset + it.length >= match.range.last + 1 }) {
- entities.add(
- MessageEntity(
- offset = match.range.first,
- length = match.range.last - match.range.first + 1,
- type = MessageEntityType.Mention
- )
- )
- }
+ addDetectedEntities(entities, URL_REGEX.findAll(text.text), MessageEntityType.Url)
+ addDetectedEntities(entities, EMAIL_REGEX.findAll(text.text), MessageEntityType.Email)
+ addDetectedEntities(
+ entities,
+ BANK_CARD_REGEX.findAll(text.text),
+ MessageEntityType.BankCardNumber
+ ) { match ->
+ val candidate = match.value.trim()
+ candidate.filter(Char::isDigit).length in 13..19 && passesLuhn(candidate)
+ }
+ addDetectedEntities(
+ entities,
+ PHONE_REGEX.findAll(text.text),
+ MessageEntityType.PhoneNumber
+ ) { match ->
+ val candidate = match.value.trim()
+ val digitCount = candidate.count(Char::isDigit)
+ digitCount in 7..15 && !passesLuhn(candidate)
}
+ addDetectedEntities(entities, HASHTAG_REGEX.findAll(text.text), MessageEntityType.Hashtag)
+ addDetectedEntities(entities, CASHTAG_REGEX.findAll(text.text), MessageEntityType.Cashtag)
+ addDetectedEntities(
+ entities,
+ BOT_COMMAND_REGEX.findAll(text.text),
+ MessageEntityType.BotCommand
+ )
+ addDetectedEntities(entities, MENTION_REGEX.findAll(text.text), MessageEntityType.Mention)
return entities
.sortedWith(compareBy { it.offset }.thenByDescending { it.length })
@@ -827,9 +839,21 @@ internal fun richEntityToAnnotation(type: MessageEntityType): String? {
is MessageEntityType.Underline -> ENTITY_UNDERLINE
is MessageEntityType.Strikethrough -> ENTITY_STRIKE
is MessageEntityType.Spoiler -> ENTITY_SPOILER
+ is MessageEntityType.BlockQuote -> ENTITY_BLOCK_QUOTE
+ is MessageEntityType.BlockQuoteExpandable -> ENTITY_BLOCK_QUOTE_EXPANDABLE
is MessageEntityType.Code -> ENTITY_CODE
is MessageEntityType.Pre -> if (type.language.isBlank()) ENTITY_PRE else "$ENTITY_PRE:${type.language}"
is MessageEntityType.TextUrl -> "$ENTITY_TEXT_URL:${type.url}"
+ is MessageEntityType.Mention -> ENTITY_MENTION
+ is MessageEntityType.Hashtag -> ENTITY_HASHTAG
+ is MessageEntityType.Cashtag -> ENTITY_CASHTAG
+ is MessageEntityType.BotCommand -> ENTITY_BOT_COMMAND
+ is MessageEntityType.Url -> ENTITY_URL
+ is MessageEntityType.Email -> ENTITY_EMAIL
+ is MessageEntityType.PhoneNumber -> ENTITY_PHONE_NUMBER
+ is MessageEntityType.BankCardNumber -> ENTITY_BANK_CARD_NUMBER
+ is MessageEntityType.DateTime -> "$ENTITY_DATE_TIME:${type.unixTime}"
+ is MessageEntityType.MediaTimestamp -> "$ENTITY_MEDIA_TIMESTAMP:${type.mediaTimestampSeconds}"
else -> null
}
}
@@ -841,10 +865,27 @@ internal fun decodeRichEntity(value: String): MessageEntityType? {
value == ENTITY_UNDERLINE -> MessageEntityType.Underline
value == ENTITY_STRIKE -> MessageEntityType.Strikethrough
value == ENTITY_SPOILER -> MessageEntityType.Spoiler
+ value == ENTITY_BLOCK_QUOTE -> MessageEntityType.BlockQuote
+ value == ENTITY_BLOCK_QUOTE_EXPANDABLE -> MessageEntityType.BlockQuoteExpandable
value == ENTITY_CODE -> MessageEntityType.Code
value == ENTITY_PRE -> MessageEntityType.Pre()
value.startsWith("$ENTITY_PRE:") -> MessageEntityType.Pre(value.substringAfter(':'))
value.startsWith("$ENTITY_TEXT_URL:") -> MessageEntityType.TextUrl(value.substringAfter(':'))
+ value == ENTITY_MENTION -> MessageEntityType.Mention
+ value == ENTITY_HASHTAG -> MessageEntityType.Hashtag
+ value == ENTITY_CASHTAG -> MessageEntityType.Cashtag
+ value == ENTITY_BOT_COMMAND -> MessageEntityType.BotCommand
+ value == ENTITY_URL -> MessageEntityType.Url
+ value == ENTITY_EMAIL -> MessageEntityType.Email
+ value == ENTITY_PHONE_NUMBER -> MessageEntityType.PhoneNumber
+ value == ENTITY_BANK_CARD_NUMBER -> MessageEntityType.BankCardNumber
+ value.startsWith("$ENTITY_DATE_TIME:") -> MessageEntityType.DateTime(
+ value.substringAfter(':').toIntOrNull() ?: 0
+ )
+
+ value.startsWith("$ENTITY_MEDIA_TIMESTAMP:") -> MessageEntityType.MediaTimestamp(
+ value.substringAfter(':').toIntOrNull() ?: 0
+ )
else -> null
}
}
@@ -852,15 +893,60 @@ internal fun decodeRichEntity(value: String): MessageEntityType? {
private fun MessageEntityType.toEditorStyle(primaryColor: Color): SpanStyle? {
val codeBackground = primaryColor.copy(alpha = 0.14f)
val spoilerBackground = primaryColor.copy(alpha = 0.25f)
+ val quoteBackground = primaryColor.copy(alpha = 0.09f)
return when (this) {
is MessageEntityType.Bold -> SpanStyle(fontWeight = FontWeight.Bold)
is MessageEntityType.Italic -> SpanStyle(fontStyle = FontStyle.Italic)
is MessageEntityType.Underline -> SpanStyle(textDecoration = TextDecoration.Underline)
is MessageEntityType.Strikethrough -> SpanStyle(textDecoration = TextDecoration.LineThrough)
is MessageEntityType.Spoiler -> SpanStyle(background = spoilerBackground)
+ is MessageEntityType.BlockQuote -> SpanStyle(
+ color = primaryColor,
+ background = quoteBackground,
+ fontStyle = FontStyle.Italic
+ )
+
+ is MessageEntityType.BlockQuoteExpandable -> SpanStyle(
+ color = primaryColor,
+ background = quoteBackground,
+ fontStyle = FontStyle.Italic
+ )
is MessageEntityType.Code -> SpanStyle(fontFamily = FontFamily.Monospace, background = codeBackground)
is MessageEntityType.Pre -> SpanStyle(fontFamily = FontFamily.Monospace, background = codeBackground)
is MessageEntityType.TextUrl -> SpanStyle(color = primaryColor, textDecoration = TextDecoration.Underline)
+ is MessageEntityType.Mention -> SpanStyle(color = primaryColor)
+ is MessageEntityType.Hashtag -> SpanStyle(color = primaryColor)
+ is MessageEntityType.Cashtag -> SpanStyle(color = primaryColor)
+ is MessageEntityType.BotCommand -> SpanStyle(color = primaryColor)
+ is MessageEntityType.Url -> SpanStyle(
+ color = primaryColor,
+ textDecoration = TextDecoration.Underline
+ )
+
+ is MessageEntityType.Email -> SpanStyle(
+ color = primaryColor,
+ textDecoration = TextDecoration.Underline
+ )
+
+ is MessageEntityType.PhoneNumber -> SpanStyle(
+ color = primaryColor,
+ textDecoration = TextDecoration.Underline
+ )
+
+ is MessageEntityType.BankCardNumber -> SpanStyle(
+ color = primaryColor,
+ textDecoration = TextDecoration.Underline
+ )
+
+ is MessageEntityType.DateTime -> SpanStyle(
+ color = primaryColor,
+ textDecoration = TextDecoration.Underline
+ )
+
+ is MessageEntityType.MediaTimestamp -> SpanStyle(
+ color = primaryColor,
+ textDecoration = TextDecoration.Underline
+ )
else -> null
}
}
@@ -1096,3 +1182,50 @@ private fun normalizeUrl(raw: String): String? {
if (trimmed.isEmpty()) return null
return if (trimmed.contains("://")) trimmed else "https://$trimmed"
}
+
+private fun addDetectedEntities(
+ entities: MutableList,
+ matches: Sequence,
+ type: MessageEntityType,
+ predicate: (MatchResult) -> Boolean = { true }
+) {
+ matches.forEach { match ->
+ if (!predicate(match)) return@forEach
+ val start = match.range.first
+ val end = match.range.last + 1
+ if (entities.none { it.offset < end && (it.offset + it.length) > start }) {
+ entities += MessageEntity(
+ offset = start,
+ length = end - start,
+ type = type
+ )
+ }
+ }
+}
+
+private fun passesLuhn(value: String): Boolean {
+ val digits = value.filter(Char::isDigit)
+ if (digits.length !in 13..19) return false
+
+ var sum = 0
+ var shouldDouble = false
+ for (index in digits.length - 1 downTo 0) {
+ var digit = digits[index].digitToInt()
+ if (shouldDouble) {
+ digit *= 2
+ if (digit > 9) digit -= 9
+ }
+ sum += digit
+ shouldDouble = !shouldDouble
+ }
+ return sum % 10 == 0
+}
+
+private val MENTION_REGEX = Regex("""(?()]+""")
+private val EMAIL_REGEX = Regex("""(?i)\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b""")
+private val PHONE_REGEX = Regex("""(?,
+ renderData: MessageTextRenderData,
+ style: TextStyle,
+ color: Color,
+ emojiFontFamily: FontFamily,
+ modifier: Modifier = Modifier,
+ onSpoilerClick: (Int) -> Unit = {}
+) {
+ val previewStyle = style.copy(
+ fontSize = if (renderData.isBigEmoji) (style.fontSize.value * 5f).sp else style.fontSize,
+ lineHeight = if (renderData.isBigEmoji) (style.fontSize.value * 5.5f).sp else style.lineHeight
+ )
+
+ if (renderData.isBigEmoji && renderData.bigEmojiItems.isNotEmpty()) {
+ BigEmojiContent(
+ items = renderData.bigEmojiItems,
+ sizeDp = style.fontSize.value * 5f,
+ modifier = modifier,
+ emojiFontFamily = emojiFontFamily
+ )
+ } else {
+ MessageText(
+ text = renderData.annotatedText,
+ rawText = rawText,
+ inlineContent = renderData.inlineContent,
+ entities = entities,
+ style = previewStyle,
+ color = color,
+ modifier = modifier,
+ onSpoilerClick = onSpoilerClick
+ )
+ }
+}
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/AudioMessageBubble.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/AudioMessageBubble.kt
index fd60a4d51..c5552ab0a 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/AudioMessageBubble.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/AudioMessageBubble.kt
@@ -4,7 +4,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@@ -136,7 +135,6 @@ fun AudioMessageBubble(
Column(
modifier = Modifier
.padding(start = 8.dp, end = 8.dp, top = 8.dp, bottom = 6.dp)
- .width(IntrinsicSize.Max)
.widthIn(min = 184.dp, max = 300.dp)
) {
if (isGroup && !isOutgoing && !isSameSenderAbove) {
@@ -361,7 +359,6 @@ fun AudioAlbumBubble(
Column(
modifier = Modifier
.padding(start = 8.dp, end = 8.dp, top = 8.dp, bottom = 6.dp)
- .width(IntrinsicSize.Max)
.widthIn(min = 200.dp, max = 300.dp)
) {
if (isGroup && !isOutgoing && !isSameSenderAbove) {
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/DocumentMessageBubble.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/DocumentMessageBubble.kt
index 50baa4c8f..bbb26bacc 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/DocumentMessageBubble.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/DocumentMessageBubble.kt
@@ -6,7 +6,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@@ -150,7 +149,6 @@ fun DocumentMessageBubble(
Column(
modifier = Modifier
.padding(start = 8.dp, end = 8.dp, top = 8.dp, bottom = 6.dp)
- .width(IntrinsicSize.Max)
.widthIn(min = 184.dp, max = 300.dp)
) {
if (isGroup && !isOutgoing && !isSameSenderAbove) {
@@ -384,7 +382,6 @@ fun DocumentAlbumBubble(
Column(
modifier = Modifier
.padding(start = 8.dp, end = 8.dp, top = 8.dp, bottom = 6.dp)
- .width(IntrinsicSize.Max)
.widthIn(min = 200.dp, max = 300.dp)
) {
if (isGroup && !isOutgoing && !isSameSenderAbove) {
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/LinkPreview.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/LinkPreview.kt
index cb9c6ffc1..8fb3b135d 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/LinkPreview.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/LinkPreview.kt
@@ -5,12 +5,10 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
-import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -33,6 +31,9 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
@@ -83,62 +84,64 @@ internal fun LinkPreview(
else colorScheme.onSurface.copy(alpha = 0.05f)
)
) {
- Row(modifier = Modifier.height(IntrinsicSize.Min)) {
- Box(
- modifier = Modifier
- .width(3.dp)
- .fillMaxHeight()
- .background(borderColor)
- )
- Column(modifier = Modifier.padding(8.dp)) {
- if (resolved.isSmallMedia) {
- Row {
- Column(
- modifier = Modifier
- .weight(1f)
- .previewTapTarget(
- onTap = { onAction(resolved.primaryAction) },
- onLongClick = onLongClick
- )
- ) {
- LinkPreviewTextContent(
- meta = resolved.meta,
- isOutgoing = isOutgoing
- )
- }
- Spacer(modifier = Modifier.width(8.dp))
- LinkPreviewSmallImage(
- thumbnailData = resolved.thumbnailData,
- thumbnailCacheKey = resolved.thumbnailCacheKey,
- onTap = { onAction(resolved.mediaAction) },
- onLongClick = onLongClick
- )
- }
- } else {
+ Column(
+ modifier = Modifier
+ .drawBehind {
+ drawRect(
+ color = borderColor,
+ topLeft = Offset.Zero,
+ size = Size(3.dp.toPx(), size.height)
+ )
+ }
+ .padding(start = 11.dp, top = 8.dp, end = 8.dp, bottom = 8.dp)
+ ) {
+ if (resolved.isSmallMedia) {
+ Row {
Column(
- modifier = Modifier.previewTapTarget(
- onTap = { onAction(resolved.primaryAction) },
- onLongClick = onLongClick
- )
+ modifier = Modifier
+ .weight(1f)
+ .previewTapTarget(
+ onTap = { onAction(resolved.primaryAction) },
+ onLongClick = onLongClick
+ )
) {
LinkPreviewTextContent(
meta = resolved.meta,
isOutgoing = isOutgoing
)
}
+ Spacer(modifier = Modifier.width(8.dp))
+ LinkPreviewSmallImage(
+ thumbnailData = resolved.thumbnailData,
+ thumbnailCacheKey = resolved.thumbnailCacheKey,
+ onTap = { onAction(resolved.mediaAction) },
+ onLongClick = onLongClick
+ )
+ }
+ } else {
+ Column(
+ modifier = Modifier.previewTapTarget(
+ onTap = { onAction(resolved.primaryAction) },
+ onLongClick = onLongClick
+ )
+ ) {
+ LinkPreviewTextContent(
+ meta = resolved.meta,
+ isOutgoing = isOutgoing
+ )
+ }
- if (resolved.hasMedia) {
- Spacer(modifier = Modifier.height(8.dp))
- LinkPreviewLargeMedia(
- thumbnailData = resolved.thumbnailData,
- thumbnailCacheKey = resolved.thumbnailCacheKey,
- aspectRatio = resolved.aspectRatio,
- showPlayOverlay = resolved.showPlayOverlay,
- duration = webPage.duration,
- onTap = { onAction(resolved.mediaAction) },
- onLongClick = onLongClick
- )
- }
+ if (resolved.hasMedia) {
+ Spacer(modifier = Modifier.height(8.dp))
+ LinkPreviewLargeMedia(
+ thumbnailData = resolved.thumbnailData,
+ thumbnailCacheKey = resolved.thumbnailCacheKey,
+ aspectRatio = resolved.aspectRatio,
+ showPlayOverlay = resolved.showPlayOverlay,
+ duration = webPage.duration,
+ onTap = { onAction(resolved.mediaAction) },
+ onLongClick = onLongClick
+ )
}
}
}
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MarkdownTableBlock.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MarkdownTableBlock.kt
new file mode 100644
index 000000000..27c820007
--- /dev/null
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MarkdownTableBlock.kt
@@ -0,0 +1,183 @@
+package org.monogram.presentation.features.chats.conversation.ui.message
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+@Composable
+internal fun MarkdownTableBlock(
+ text: String,
+ isOutgoing: Boolean,
+ modifier: Modifier = Modifier
+) {
+ val rows = remember(text) { parseMarkdownTableRows(text) }
+ if (rows.isEmpty()) {
+ CodeBlock(
+ text = text,
+ language = "table",
+ isOutgoing = isOutgoing,
+ modifier = modifier
+ )
+ return
+ }
+
+ BoxWithConstraints(modifier = modifier.fillMaxWidth()) {
+ val tableWidth = remember(rows, maxWidth) {
+ resolveMarkdownTableWidth(
+ availableWidth = maxWidth,
+ columnCount = rows.maxOfOrNull { it.size } ?: 1
+ )
+ }
+ val dividerColor = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.45f)
+
+ Box(
+ modifier = Modifier.horizontalScroll(rememberScrollState())
+ ) {
+ Column(
+ modifier = Modifier
+ .width(tableWidth)
+ .clip(RoundedCornerShape(12.dp))
+ .background(MaterialTheme.colorScheme.surfaceContainerLow)
+ .padding(12.dp)
+ ) {
+ rows.forEachIndexed { rowIndex, row ->
+ val isLastRow = rowIndex == rows.lastIndex
+ val rowBackground = when {
+ rowIndex == 0 -> MaterialTheme.colorScheme.surfaceContainerHighest
+ rowIndex % 2 == 1 -> MaterialTheme.colorScheme.surfaceContainer
+ else -> Color.Transparent
+ }
+
+ androidx.compose.foundation.layout.Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .drawBehind {
+ if (!isLastRow) {
+ val lineWidth = 1.dp.toPx()
+ drawLine(
+ color = dividerColor,
+ start = Offset(0f, size.height - lineWidth / 2f),
+ end = Offset(size.width, size.height - lineWidth / 2f),
+ strokeWidth = lineWidth
+ )
+ }
+ }
+ ) {
+ row.forEachIndexed { cellIndex, cell ->
+ Box(
+ modifier = Modifier
+ .weight(1f)
+ .background(rowBackground)
+ .drawBehind {
+ if (cellIndex < row.lastIndex) {
+ val lineWidth = 1.dp.toPx()
+ drawLine(
+ color = dividerColor,
+ start = Offset(size.width - lineWidth / 2f, 0f),
+ end = Offset(
+ size.width - lineWidth / 2f,
+ size.height
+ ),
+ strokeWidth = lineWidth
+ )
+ }
+ }
+ .padding(horizontal = 10.dp, vertical = 8.dp)
+ ) {
+ Text(
+ text = cell,
+ style = if (rowIndex == 0) {
+ MaterialTheme.typography.labelLarge
+ } else {
+ MaterialTheme.typography.bodyMedium
+ },
+ fontWeight = if (rowIndex == 0) FontWeight.Bold else FontWeight.Normal,
+ color = MaterialTheme.colorScheme.onSurface,
+ textAlign = TextAlign.Start
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+private fun resolveMarkdownTableWidth(
+ availableWidth: Dp,
+ columnCount: Int
+): Dp {
+ val columnSpacing = 8.dp
+ val minColumnWidth = 120.dp
+ val minTableWidth =
+ minColumnWidth * columnCount + columnSpacing * (columnCount - 1).coerceAtLeast(0)
+ return maxOf(availableWidth, minTableWidth)
+}
+
+internal fun parseMarkdownTableRows(text: String): List> {
+ val lines = text.lineSequence()
+ .map { it.trimEnd() }
+ .filter { it.isNotBlank() }
+ .toList()
+
+ if (lines.isEmpty()) return emptyList()
+
+ val boxRows = lines.filter { it.contains('│') }
+ if (boxRows.size >= 2) {
+ return boxRows.mapNotNull { line -> parseDelimitedTableRow(line, '│') }
+ .takeIf { it.size >= 2 && it.allSameSize() }
+ .orEmpty()
+ }
+
+ val pipeRows = lines
+ .filterNot { isMarkdownTableSeparatorRow(it) }
+ .filter { it.contains('|') }
+
+ return pipeRows.mapNotNull { line -> parseDelimitedTableRow(line, '|') }
+ .takeIf { it.size >= 2 && it.allSameSize() }
+ .orEmpty()
+}
+
+private fun parseDelimitedTableRow(line: String, delimiter: Char): List? {
+ val parts = line.split(delimiter).map { it.trim() }
+ val normalized = when {
+ parts.size >= 2 && parts.first().isEmpty() && parts.last().isEmpty() -> parts.drop(1)
+ .dropLast(1)
+
+ else -> parts
+ }
+ return normalized.takeIf { it.size >= 2 }
+ ?.let { if (it.any { cell -> cell.isNotEmpty() }) it else null }
+}
+
+private fun List>.allSameSize(): Boolean {
+ val firstSize = firstOrNull()?.size ?: return false
+ return all { it.size == firstSize }
+}
+
+private fun isMarkdownTableSeparatorRow(line: String): Boolean {
+ val compact = line.trim()
+ if (compact.isEmpty()) return false
+ return compact.all { it == '|' || it == ':' || it == '-' || it.isWhitespace() }
+}
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MediaBubbleSizing.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MediaBubbleSizing.kt
index 3dbd2490d..afd367b1b 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MediaBubbleSizing.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MediaBubbleSizing.kt
@@ -3,6 +3,7 @@ package org.monogram.presentation.features.chats.conversation.ui.message
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import org.monogram.domain.models.MessageContent
+import org.monogram.domain.models.webapp.PageBlock
private const val DEFAULT_MEDIA_ASPECT_RATIO = 1f
private const val MIN_MEDIA_ASPECT_RATIO = 0.5f
@@ -10,7 +11,22 @@ private const val MAX_MEDIA_ASPECT_RATIO = 2f
private const val MEDIA_BUBBLE_WIDTH_FRACTION = 0.92f
internal fun MessageContent.prefersExpandedBubbleWidth(): Boolean =
- this is MessageContent.Photo || this is MessageContent.Video || this is MessageContent.Gif
+ this is MessageContent.Photo ||
+ this is MessageContent.Video ||
+ this is MessageContent.Gif ||
+ (this is MessageContent.RichMessage && blocks.any(PageBlock::prefersExpandedWidth))
+
+private fun PageBlock.prefersExpandedWidth(): Boolean {
+ return when (this) {
+ is PageBlock.Table -> true
+ is PageBlock.BlockQuote -> pageBlocks.any(PageBlock::prefersExpandedWidth)
+ is PageBlock.Details -> pageBlocks.any(PageBlock::prefersExpandedWidth)
+ is PageBlock.Collage -> pageBlocks.any(PageBlock::prefersExpandedWidth)
+ is PageBlock.Slideshow -> pageBlocks.any(PageBlock::prefersExpandedWidth)
+ is PageBlock.EmbeddedPost -> pageBlocks.any(PageBlock::prefersExpandedWidth)
+ else -> false
+ }
+}
internal fun resolveMediaBubbleAspectRatio(
mediaWidth: Int,
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageText.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageText.kt
index 5993e6ba6..4ea716fdb 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageText.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageText.kt
@@ -35,7 +35,7 @@ import org.koin.compose.koinInject
import org.monogram.domain.models.MessageEntity
import org.monogram.domain.models.MessageEntityType
import org.monogram.domain.repository.TelegramLinkRepository
-import org.monogram.presentation.features.chats.conversation.ui.message.model.isBlockElement
+import org.monogram.presentation.features.chats.conversation.ui.message.model.topLevelBlockEntities
internal data class MessageTextLayoutInfo(
val width: Int,
@@ -70,7 +70,7 @@ internal fun MessageText(
val linkHandler = LocalLinkHandler.current
val blockEntities = entities
- .filter { it.type.isBlockElement() }
+ .topLevelBlockEntities()
.sortedBy { it.offset }
Column(modifier = modifier) {
@@ -325,6 +325,16 @@ private fun DefaultTextRender(
consumed = true
}
+ "EMAIL" -> {
+ uriHandler.openUri("mailto:${annotation.item}")
+ consumed = true
+ }
+
+ "PHONE" -> {
+ uriHandler.openUri("tel:${annotation.item}")
+ consumed = true
+ }
+
"SPOILER", "SPOILER_REVEALED", "SPOILER_UNREVEALED" -> {
annotation.item.toIntOrNull()?.let {
onSpoilerClick(it)
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageTextFormatter.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageTextFormatter.kt
index 4ee131be5..e13201481 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageTextFormatter.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageTextFormatter.kt
@@ -400,6 +400,88 @@ fun buildAnnotatedMessageTextWithEmoji(
)
}
+ is MessageEntityType.Cashtag -> {
+ addStyle(SpanStyle(color = linkColor), start, end)
+ addStringAnnotation(
+ "CASHTAG",
+ text.safeSubstring(safeStart, safeEnd),
+ start,
+ end
+ )
+ }
+
+ is MessageEntityType.BotCommand -> {
+ addStyle(SpanStyle(color = linkColor), start, end)
+ addStringAnnotation(
+ "COPY",
+ text.safeSubstring(safeStart, safeEnd),
+ start,
+ end
+ )
+ }
+
+ is MessageEntityType.Email -> {
+ val email = text.safeSubstring(safeStart, safeEnd)
+ addStyle(
+ SpanStyle(
+ color = linkColor,
+ textDecoration = TextDecoration.Underline
+ ), start, end
+ )
+ addStringAnnotation("EMAIL", email, start, end)
+ }
+
+ is MessageEntityType.PhoneNumber -> {
+ val phone = text.safeSubstring(safeStart, safeEnd)
+ addStyle(
+ SpanStyle(
+ color = linkColor,
+ textDecoration = TextDecoration.Underline
+ ), start, end
+ )
+ addStringAnnotation("PHONE", phone, start, end)
+ }
+
+ is MessageEntityType.BankCardNumber -> {
+ addStyle(
+ SpanStyle(
+ color = linkColor,
+ textDecoration = TextDecoration.Underline
+ ), start, end
+ )
+ addStringAnnotation(
+ "COPY",
+ text.safeSubstring(safeStart, safeEnd),
+ start,
+ end
+ )
+ }
+
+ is MessageEntityType.DateTime -> {
+ addStyle(
+ SpanStyle(
+ color = linkColor,
+ textDecoration = TextDecoration.Underline
+ ), start, end
+ )
+ addStringAnnotation("DATE_TIME", type.unixTime.toString(), start, end)
+ }
+
+ is MessageEntityType.MediaTimestamp -> {
+ addStyle(
+ SpanStyle(
+ color = linkColor,
+ textDecoration = TextDecoration.Underline
+ ), start, end
+ )
+ addStringAnnotation(
+ "MEDIA_TIMESTAMP",
+ type.mediaTimestampSeconds.toString(),
+ start,
+ end
+ )
+ }
+
else -> {}
}
}
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/PollMessageBubble.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/PollMessageBubble.kt
index d0e4a9044..4ce22e0fe 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/PollMessageBubble.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/PollMessageBubble.kt
@@ -12,7 +12,6 @@ import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
@@ -137,7 +136,6 @@ fun PollMessageBubble(
Column(
modifier = modifier
- .width(IntrinsicSize.Max)
.widthIn(min = 240.dp, max = 332.dp)
.pointerInput(Unit) {
detectTapGestures(
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/QuoteBlock.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/QuoteBlock.kt
index 9f3d5cd3b..e55abed4f 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/QuoteBlock.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/QuoteBlock.kt
@@ -6,11 +6,8 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
@@ -29,6 +26,10 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -75,7 +76,6 @@ internal fun QuoteBlock(
Surface(
modifier = Modifier
- .height(IntrinsicSize.Min)
.animateContentSize()
.clickable(
interactionSource = null,
@@ -84,7 +84,17 @@ internal fun QuoteBlock(
) {
isCollapsed = !isCollapsed
}
- .padding(vertical = 4.dp),
+ .padding(vertical = 4.dp)
+ .drawBehind {
+ val stripeWidth = 3.dp.toPx()
+ val stripeRadius = 999.dp.toPx()
+ drawRoundRect(
+ color = stripeColor,
+ topLeft = Offset(8.dp.toPx(), 10.dp.toPx()),
+ size = Size(stripeWidth, size.height - 20.dp.toPx()),
+ cornerRadius = CornerRadius(stripeRadius, stripeRadius)
+ )
+ },
shape = RoundedCornerShape(12.dp),
color = background
) {
@@ -92,16 +102,7 @@ internal fun QuoteBlock(
modifier = Modifier.padding(start = 8.dp, end = 10.dp, top = 8.dp, bottom = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
- Spacer(
- Modifier
- .width(3.dp)
- .padding(vertical = 2.dp)
- .fillMaxHeight()
- .background(
- color = stripeColor,
- shape = RoundedCornerShape(999.dp)
- )
- )
+ Spacer(modifier = Modifier.width(6.dp))
val textModifier = Modifier
.weight(1f, fill = false)
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/RichMessageBubble.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/RichMessageBubble.kt
index 1c7ba0044..446326f28 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/RichMessageBubble.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/RichMessageBubble.kt
@@ -3,9 +3,7 @@ package org.monogram.presentation.features.chats.conversation.ui.message
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
@@ -140,7 +138,6 @@ internal fun RichMessageBubble(
Column(
modifier = modifier
- .width(IntrinsicSize.Max)
.widthIn(min = 120.dp),
horizontalAlignment = if (isOutgoing) Alignment.End else Alignment.Start
) {
@@ -187,6 +184,9 @@ internal fun RichMessageBubble(
block = renderModel.block,
textSizeMultiplier = (fontSize / 16f).coerceIn(0.75f, 1.5f),
stateKeyPrefix = renderModel.stateKeyPrefix,
+ useMessageQuoteStyle = true,
+ onRichTextTap = onClick,
+ onRichTextLongPress = onLongClick,
onOpenPhotoFullscreen = { photo, caption ->
openRichPhoto(
photo = photo,
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/TextBlocks.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/TextBlocks.kt
index d3362ea24..0965401f7 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/TextBlocks.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/TextBlocks.kt
@@ -4,7 +4,7 @@ import androidx.compose.runtime.Composable
import org.monogram.domain.models.MessageEntity
import org.monogram.domain.models.MessageEntityType
import org.monogram.presentation.features.chats.conversation.ui.message.model.blockFor
-import org.monogram.presentation.features.chats.conversation.ui.message.model.inlineEntitiesForBlock
+import org.monogram.presentation.features.chats.conversation.ui.message.model.entitiesForBlock
@Composable
internal fun TextBlocks(
@@ -15,16 +15,23 @@ internal fun TextBlocks(
) {
when (val type = entity.type) {
is MessageEntityType.Pre -> {
- CodeBlock(
- text = text blockFor entity,
- language = type.language,
- isOutgoing = isOutgoing
- )
+ if (type.language.equals("table", ignoreCase = true)) {
+ MarkdownTableBlock(
+ text = text blockFor entity,
+ isOutgoing = isOutgoing
+ )
+ } else {
+ CodeBlock(
+ text = text blockFor entity,
+ language = type.language,
+ isOutgoing = isOutgoing
+ )
+ }
}
is MessageEntityType.BlockQuote -> {
QuoteBlock(
text = text blockFor entity,
- entities = entities.inlineEntitiesForBlock(entity),
+ entities = entities.entitiesForBlock(entity),
isOutgoing = isOutgoing,
expandable = false,
)
@@ -32,7 +39,7 @@ internal fun TextBlocks(
is MessageEntityType.BlockQuoteExpandable -> {
QuoteBlock(
text = text blockFor entity,
- entities = entities.inlineEntitiesForBlock(entity),
+ entities = entities.entitiesForBlock(entity),
isOutgoing = isOutgoing,
expandable = true,
)
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/TextMessageBubble.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/TextMessageBubble.kt
index 25b6f09e7..03db480aa 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/TextMessageBubble.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/TextMessageBubble.kt
@@ -1,9 +1,7 @@
package org.monogram.presentation.features.chats.conversation.ui.message
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
@@ -78,7 +76,6 @@ internal fun TextMessageBubble(
Column(
modifier = modifier
- .width(IntrinsicSize.Max)
.widthIn(min = 60.dp),
horizontalAlignment = if (isOutgoing) Alignment.End else Alignment.Start
) {
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/VoiceMessageBubble.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/VoiceMessageBubble.kt
index b81a42099..3fee48f89 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/VoiceMessageBubble.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/VoiceMessageBubble.kt
@@ -5,7 +5,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@@ -136,7 +135,6 @@ fun VoiceMessageBubble(
Column(
modifier = Modifier
.padding(start = 8.dp, end = 8.dp, top = 8.dp, bottom = 6.dp)
- .width(IntrinsicSize.Max)
.widthIn(min = 184.dp, max = 300.dp)
) {
if (isGroup && !isOutgoing && !isSameSenderAbove) {
diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/model/Mappers.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/model/Mappers.kt
index 2ccccb721..15e2eaa0c 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/model/Mappers.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/model/Mappers.kt
@@ -10,6 +10,45 @@ internal infix fun String.blockFor(entity: MessageEntity): String =
safeSubstring(entity.offset, entity.offset.toLong() + entity.length.toLong())
internal fun List.inlineEntitiesForBlock(blockEntity: MessageEntity): List {
+ return entitiesForBlock(blockEntity).filterNot { it.type.isBlockElement() }
+}
+
+internal fun List.topLevelBlockEntities(): List {
+ val blockEntities = asSequence()
+ .filter { it.type.isBlockElement() }
+ .sortedWith(compareBy { it.offset }.thenByDescending { it.length })
+ .toList()
+
+ return blockEntities.filter { entity ->
+ val entityStart = entity.offset
+ val entityEnd = (entity.offset.toLong() + entity.length.toLong())
+ .coerceAtLeast(entityStart.toLong())
+ .coerceAtMost(Int.MAX_VALUE.toLong())
+ .toInt()
+
+ blockEntities.none { parent ->
+ if (parent == entity) return@none false
+
+ val parentStart = parent.offset
+ val parentEnd = (parent.offset.toLong() + parent.length.toLong())
+ .coerceAtLeast(parentStart.toLong())
+ .coerceAtMost(Int.MAX_VALUE.toLong())
+ .toInt()
+
+ val strictlyContains = parentStart <= entityStart &&
+ parentEnd >= entityEnd &&
+ (parentStart < entityStart || parentEnd > entityEnd)
+ val sameRangeQuoteContainer = parentStart == entityStart &&
+ parentEnd == entityEnd &&
+ parent.type.isQuoteBlockElement() &&
+ !entity.type.isQuoteBlockElement()
+
+ strictlyContains || sameRangeQuoteContainer
+ }
+ }
+}
+
+internal fun List.entitiesForBlock(blockEntity: MessageEntity): List {
val blockStart = blockEntity.offset
val blockEnd = (blockEntity.offset.toLong() + blockEntity.length.toLong())
.coerceAtLeast(blockStart.toLong())
@@ -18,7 +57,6 @@ internal fun List.inlineEntitiesForBlock(blockEntity: MessageEnti
return asSequence()
.filterNot { it == blockEntity }
- .filterNot { it.type.isBlockElement() }
.mapNotNull { entity ->
val start = entity.offset
val end = (entity.offset.toLong() + entity.length.toLong())
@@ -50,4 +88,13 @@ internal fun MessageEntityType.isBlockElement(): Boolean {
is MessageEntityType.BlockQuoteExpandable -> true
else -> false
}
-}
\ No newline at end of file
+}
+
+private fun MessageEntityType.isQuoteBlockElement(): Boolean {
+ return when (this) {
+ is MessageEntityType.BlockQuote,
+ is MessageEntityType.BlockQuoteExpandable -> true
+
+ else -> false
+ }
+}
diff --git a/presentation/src/main/java/org/monogram/presentation/features/instantview/InstantViewer.kt b/presentation/src/main/java/org/monogram/presentation/features/instantview/InstantViewer.kt
index c31348bd8..bfcdd5494 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/instantview/InstantViewer.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/instantview/InstantViewer.kt
@@ -17,25 +17,22 @@ import androidx.compose.animation.scaleOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
-import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.basicMarquee
-import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.aspectRatio
-import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -44,7 +41,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
@@ -109,6 +105,10 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.TransformOrigin
@@ -131,6 +131,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.koin.compose.koinInject
import org.monogram.domain.models.WebPage
+import org.monogram.domain.models.webapp.HorizontalAlignment
import org.monogram.domain.models.webapp.InstantViewModel
import org.monogram.domain.models.webapp.PageBlock
import org.monogram.domain.models.webapp.PageBlockCaption
@@ -551,6 +552,7 @@ internal fun PageBlock.stableBlockSignature(): String {
is PageBlock.VideoBlock -> "video:${video.fileId}:${caption.stableCaptionSignature()}:${video.duration}"
is PageBlock.AnimationBlock -> "animation:${animation.fileId}:${caption.stableCaptionSignature()}:${animation.duration}"
is PageBlock.AudioBlock -> "audio:${audio.fileId}:${audio.title.orEmpty()}"
+ is PageBlock.VoiceNoteBlock -> "voice:${voiceNote.fileId}:${voiceNote.duration}:${caption.stableCaptionSignature()}"
is PageBlock.Embedded -> "embedded:$url:${posterPhoto?.fileId ?: 0}"
is PageBlock.EmbeddedPost -> "embedded_post:$author:$date:${authorPhoto?.fileId ?: 0}"
is PageBlock.Details -> "details:${richTextPlainText(header)}:${pageBlocks.joinToString("|") { it.stableBlockSignature() }}"
@@ -573,12 +575,30 @@ internal fun PageBlock.stableBlockSignature(): String {
private fun PageBlockCaption.stableCaptionSignature(): String = renderedTextOrNull().orEmpty()
+internal fun resolveInstantViewTableWidth(
+ availableWidth: androidx.compose.ui.unit.Dp,
+ columnCount: Int
+): androidx.compose.ui.unit.Dp {
+ val columnSpacing = 8.dp
+ val minColumnWidth = 120.dp
+ val minTableWidth =
+ minColumnWidth * columnCount + columnSpacing * (columnCount - 1).coerceAtLeast(0)
+ return if (availableWidth.value.isFinite()) {
+ maxOf(availableWidth, minTableWidth)
+ } else {
+ minTableWidth
+ }
+}
+
@Composable
fun InstantViewBlock(
block: PageBlock,
textSizeMultiplier: Float,
searchQuery: String = "",
stateKeyPrefix: String = "instant_view_block",
+ useMessageQuoteStyle: Boolean = false,
+ onRichTextTap: ((Offset) -> Unit)? = null,
+ onRichTextLongPress: ((Offset) -> Unit)? = null,
onOpenPhotoFullscreen: (WebPage.Photo, PageBlockCaption) -> Unit = { _, _ -> },
onOpenVideoFullscreen: (String?, Int, Boolean, String?) -> Unit = { _, _, _, _ -> },
onOpenFileExternally: (String?, Int) -> Unit = { _, _ -> }
@@ -594,14 +614,18 @@ fun InstantViewBlock(
style = MaterialTheme.typography.displaySmall,
textSizeMultiplier = textSizeMultiplier,
fontWeight = FontWeight.Bold,
- color = MaterialTheme.colorScheme.onSurface
+ color = MaterialTheme.colorScheme.onSurface,
+ onClick = onRichTextTap,
+ onLongClick = onRichTextLongPress
)
is PageBlock.Subtitle -> RichTextView(
richText = block.subtitle,
style = MaterialTheme.typography.headlineSmall,
textSizeMultiplier = textSizeMultiplier,
- color = MaterialTheme.colorScheme.primary
+ color = MaterialTheme.colorScheme.primary,
+ onClick = onRichTextTap,
+ onLongClick = onRichTextLongPress
)
is PageBlock.AuthorDate -> {
@@ -619,7 +643,9 @@ fun InstantViewBlock(
style = MaterialTheme.typography.labelLarge,
textSizeMultiplier = textSizeMultiplier,
fontWeight = FontWeight.Bold,
- color = MaterialTheme.colorScheme.onSurface
+ color = MaterialTheme.colorScheme.onSurface,
+ onClick = onRichTextTap,
+ onLongClick = onRichTextLongPress
)
if (dateStr != null) {
@@ -645,7 +671,9 @@ fun InstantViewBlock(
textSizeMultiplier = textSizeMultiplier,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(top = 16.dp),
- color = MaterialTheme.colorScheme.onSurface
+ color = MaterialTheme.colorScheme.onSurface,
+ onClick = onRichTextTap,
+ onLongClick = onRichTextLongPress
)
is PageBlock.Subheader -> RichTextView(
@@ -654,7 +682,9 @@ fun InstantViewBlock(
textSizeMultiplier = textSizeMultiplier,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(top = 8.dp),
- color = MaterialTheme.colorScheme.onSurface
+ color = MaterialTheme.colorScheme.onSurface,
+ onClick = onRichTextTap,
+ onLongClick = onRichTextLongPress
)
is PageBlock.SectionHeading -> RichTextView(
@@ -670,7 +700,9 @@ fun InstantViewBlock(
textSizeMultiplier = textSizeMultiplier,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(top = 8.dp),
- color = MaterialTheme.colorScheme.onSurface
+ color = MaterialTheme.colorScheme.onSurface,
+ onClick = onRichTextTap,
+ onLongClick = onRichTextLongPress
)
is PageBlock.Kicker -> RichTextView(
@@ -678,14 +710,18 @@ fun InstantViewBlock(
style = MaterialTheme.typography.labelLarge,
textSizeMultiplier = textSizeMultiplier,
color = MaterialTheme.colorScheme.primary,
- fontWeight = FontWeight.Bold
+ fontWeight = FontWeight.Bold,
+ onClick = onRichTextTap,
+ onLongClick = onRichTextLongPress
)
is PageBlock.Paragraph -> RichTextView(
richText = block.text,
style = MaterialTheme.typography.bodyLarge.copy(lineHeight = 32.sp),
textSizeMultiplier = textSizeMultiplier,
- color = MaterialTheme.colorScheme.onSurface
+ color = MaterialTheme.colorScheme.onSurface,
+ onClick = onRichTextTap,
+ onLongClick = onRichTextLongPress
)
is PageBlock.Preformatted -> Surface(
@@ -698,7 +734,9 @@ fun InstantViewBlock(
style = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace),
textSizeMultiplier = textSizeMultiplier,
modifier = Modifier.padding(16.dp),
- color = MaterialTheme.colorScheme.onSurface
+ color = MaterialTheme.colorScheme.onSurface,
+ onClick = onRichTextTap,
+ onLongClick = onRichTextLongPress
)
}
@@ -706,7 +744,9 @@ fun InstantViewBlock(
richText = block.footer,
style = MaterialTheme.typography.bodySmall,
textSizeMultiplier = textSizeMultiplier,
- color = MaterialTheme.colorScheme.onSurfaceVariant
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ onClick = onRichTextTap,
+ onLongClick = onRichTextLongPress
)
is PageBlock.Thinking -> Surface(
@@ -718,7 +758,9 @@ fun InstantViewBlock(
style = MaterialTheme.typography.bodyMedium,
textSizeMultiplier = textSizeMultiplier,
color = MaterialTheme.colorScheme.onSurfaceVariant,
- modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp)
+ modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
+ onClick = onRichTextTap,
+ onLongClick = onRichTextLongPress
)
}
@@ -824,8 +866,7 @@ fun InstantViewBlock(
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
- color = MaterialTheme.colorScheme.surfaceContainer,
- tonalElevation = 2.dp
+ color = MaterialTheme.colorScheme.surfaceContainerHigh
) {
Column {
ListItem(
@@ -872,6 +913,64 @@ fun InstantViewBlock(
}
}
+ is PageBlock.VoiceNoteBlock -> {
+ Surface(
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(12.dp),
+ color = MaterialTheme.colorScheme.surfaceContainerHigh
+ ) {
+ Column {
+ ListItem(
+ modifier = Modifier.clickable {
+ onOpenFileExternally(block.voiceNote.path, block.voiceNote.fileId)
+ },
+ headlineContent = {
+ Text(
+ text = stringResource(R.string.instant_view_voice_note),
+ fontWeight = FontWeight.SemiBold
+ )
+ },
+ supportingContent = block.voiceNote.mimeType?.takeIf { it.isNotBlank() }
+ ?.let { mimeType ->
+ { Text(mimeType) }
+ },
+ leadingContent = {
+ IconButton(
+ onClick = {
+ onOpenFileExternally(
+ block.voiceNote.path,
+ block.voiceNote.fileId
+ )
+ },
+ colors = IconButtonDefaults.filledIconButtonColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ contentColor = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ ) {
+ Icon(
+ Icons.Rounded.PlayArrow,
+ contentDescription = stringResource(R.string.instant_view_play)
+ )
+ }
+ },
+ trailingContent = {
+ val duration = block.voiceNote.duration
+ Text(
+ "${duration / 60}:${(duration % 60).toString().padStart(2, '0')}",
+ style = MaterialTheme.typography.labelMedium
+ )
+ },
+ colors = ListItemDefaults.colors(containerColor = Color.Transparent)
+ )
+ PageBlockCaptionView(
+ block.caption,
+ textSizeMultiplier,
+ modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
+ )
+ }
+ }
+ }
+
is PageBlock.Embedded -> {
Column(modifier = Modifier.fillMaxWidth()) {
Box(
@@ -909,8 +1008,7 @@ fun InstantViewBlock(
is PageBlock.EmbeddedPost -> {
Card(
modifier = Modifier.fillMaxWidth(),
- colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLow),
- border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerHigh)
) {
Column(modifier = Modifier.padding(16.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
@@ -951,6 +1049,9 @@ fun InstantViewBlock(
textSizeMultiplier = textSizeMultiplier,
searchQuery = searchQuery,
stateKeyPrefix = "$blockStateKey:list:$index",
+ useMessageQuoteStyle = useMessageQuoteStyle,
+ onRichTextTap = onRichTextTap,
+ onRichTextLongPress = onRichTextLongPress,
onOpenPhotoFullscreen = onOpenPhotoFullscreen,
onOpenVideoFullscreen = onOpenVideoFullscreen,
onOpenFileExternally = onOpenFileExternally
@@ -962,35 +1063,90 @@ fun InstantViewBlock(
}
is PageBlock.BlockQuote -> {
- Row(modifier = Modifier
- .fillMaxWidth()
- .height(IntrinsicSize.Min)) {
- Box(
- modifier = Modifier
- .width(4.dp)
- .fillMaxHeight()
- .background(MaterialTheme.colorScheme.primary, RoundedCornerShape(2.dp))
- )
- Spacer(modifier = Modifier.width(16.dp))
- Column {
- RichTextView(
- richText = block.text,
- style = MaterialTheme.typography.bodyLarge,
- textSizeMultiplier = textSizeMultiplier,
- fontStyle = FontStyle.Italic,
- color = MaterialTheme.colorScheme.onSurface
- )
+ val quoteStripeColor = MaterialTheme.colorScheme.primary
+ val quoteContent: @Composable () -> Unit = {
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ block.pageBlocks.forEachIndexed { index, innerBlock ->
+ InstantViewBlock(
+ block = innerBlock,
+ textSizeMultiplier = textSizeMultiplier,
+ searchQuery = searchQuery,
+ stateKeyPrefix = "$blockStateKey:quote:$index",
+ useMessageQuoteStyle = useMessageQuoteStyle,
+ onRichTextTap = onRichTextTap,
+ onRichTextLongPress = onRichTextLongPress,
+ onOpenPhotoFullscreen = onOpenPhotoFullscreen,
+ onOpenVideoFullscreen = onOpenVideoFullscreen,
+ onOpenFileExternally = onOpenFileExternally
+ )
+ }
if (block.credit !is RichText.Plain || (block.credit as RichText.Plain).text.isNotEmpty()) {
RichTextView(
richText = block.credit,
style = MaterialTheme.typography.labelMedium,
textSizeMultiplier = textSizeMultiplier,
color = MaterialTheme.colorScheme.onSurfaceVariant,
- modifier = Modifier.padding(top = 8.dp)
+ modifier = Modifier.padding(top = 4.dp),
+ onClick = onRichTextTap,
+ onLongClick = onRichTextLongPress
)
}
}
}
+
+ if (useMessageQuoteStyle) {
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 4.dp)
+ .drawBehind {
+ val stripeWidth = 3.dp.toPx()
+ val stripeRadius = 999.dp.toPx()
+ drawRoundRect(
+ color = quoteStripeColor.copy(alpha = 0.82f),
+ topLeft = Offset(8.dp.toPx(), 10.dp.toPx()),
+ size = Size(stripeWidth, size.height - 20.dp.toPx()),
+ cornerRadius = CornerRadius(stripeRadius, stripeRadius)
+ )
+ },
+ shape = RoundedCornerShape(12.dp),
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.045f)
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 18.dp, end = 12.dp, top = 10.dp, bottom = 10.dp)
+ ) {
+ quoteContent()
+ }
+ }
+ } else {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .drawBehind {
+ val stripeWidth = 4.dp.toPx()
+ val stripeRadius = 2.dp.toPx()
+ drawRoundRect(
+ color = quoteStripeColor,
+ topLeft = Offset.Zero,
+ size = Size(stripeWidth, size.height),
+ cornerRadius = CornerRadius(stripeRadius, stripeRadius)
+ )
+ }
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 20.dp)
+ ) {
+ quoteContent()
+ }
+ }
+ }
}
is PageBlock.PullQuote -> {
@@ -1006,7 +1162,9 @@ fun InstantViewBlock(
textSizeMultiplier = textSizeMultiplier,
fontStyle = FontStyle.Italic,
textAlign = TextAlign.Center,
- color = MaterialTheme.colorScheme.onSurface
+ color = MaterialTheme.colorScheme.onSurface,
+ onClick = onRichTextTap,
+ onLongClick = onRichTextLongPress
)
if (block.credit !is RichText.Plain || (block.credit as RichText.Plain).text.isNotEmpty()) {
RichTextView(
@@ -1014,7 +1172,9 @@ fun InstantViewBlock(
style = MaterialTheme.typography.labelMedium,
textSizeMultiplier = textSizeMultiplier,
color = MaterialTheme.colorScheme.onSurfaceVariant,
- modifier = Modifier.padding(top = 12.dp)
+ modifier = Modifier.padding(top = 12.dp),
+ onClick = onRichTextTap,
+ onLongClick = onRichTextLongPress
)
}
}
@@ -1022,7 +1182,10 @@ fun InstantViewBlock(
is PageBlock.ListBlock -> Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
block.items.forEach { item ->
- Row(verticalAlignment = Alignment.Top) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.Top
+ ) {
Text(
text = if (item.label.isNotEmpty()) "${item.label} " else "• ",
style = MaterialTheme.typography.bodyLarge.copy(
@@ -1031,13 +1194,19 @@ fun InstantViewBlock(
modifier = Modifier.padding(end = 8.dp),
color = MaterialTheme.colorScheme.primary
)
- Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
+ Column(
+ modifier = Modifier.weight(1f),
+ verticalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
item.pageBlocks.forEachIndexed { index, innerBlock ->
InstantViewBlock(
block = innerBlock,
textSizeMultiplier = textSizeMultiplier,
searchQuery = searchQuery,
stateKeyPrefix = "$blockStateKey:list_item:$index",
+ useMessageQuoteStyle = useMessageQuoteStyle,
+ onRichTextTap = onRichTextTap,
+ onRichTextLongPress = onRichTextLongPress,
onOpenPhotoFullscreen = onOpenPhotoFullscreen,
onOpenVideoFullscreen = onOpenVideoFullscreen,
onOpenFileExternally = onOpenFileExternally
@@ -1053,6 +1222,9 @@ fun InstantViewBlock(
textSizeMultiplier = textSizeMultiplier,
searchQuery = searchQuery,
stateKeyPrefix = "$blockStateKey:cover",
+ useMessageQuoteStyle = useMessageQuoteStyle,
+ onRichTextTap = onRichTextTap,
+ onRichTextLongPress = onRichTextLongPress,
onOpenPhotoFullscreen = onOpenPhotoFullscreen,
onOpenVideoFullscreen = onOpenVideoFullscreen,
onOpenFileExternally = onOpenFileExternally
@@ -1071,8 +1243,7 @@ fun InstantViewBlock(
Surface(
modifier = Modifier.animateContentSize(animationSpec = spring(stiffness = Spring.StiffnessLow)),
shape = RoundedCornerShape(12.dp),
- color = MaterialTheme.colorScheme.surfaceContainer,
- tonalElevation = 1.dp
+ color = MaterialTheme.colorScheme.surfaceContainerHigh
) {
Column {
Row(
@@ -1088,7 +1259,9 @@ fun InstantViewBlock(
richText = block.header,
style = MaterialTheme.typography.titleMedium,
textSizeMultiplier = textSizeMultiplier,
- fontWeight = FontWeight.SemiBold
+ fontWeight = FontWeight.SemiBold,
+ onClick = onRichTextTap,
+ onLongClick = onRichTextLongPress
)
}
Icon(
@@ -1109,6 +1282,9 @@ fun InstantViewBlock(
textSizeMultiplier = textSizeMultiplier,
searchQuery = searchQuery,
stateKeyPrefix = "$blockStateKey:details:$index",
+ useMessageQuoteStyle = useMessageQuoteStyle,
+ onRichTextTap = onRichTextTap,
+ onRichTextLongPress = onRichTextLongPress,
onOpenPhotoFullscreen = onOpenPhotoFullscreen,
onOpenVideoFullscreen = onOpenVideoFullscreen,
onOpenFileExternally = onOpenFileExternally
@@ -1128,6 +1304,9 @@ fun InstantViewBlock(
textSizeMultiplier = textSizeMultiplier,
searchQuery = searchQuery,
stateKeyPrefix = "$blockStateKey:collage:$index",
+ useMessageQuoteStyle = useMessageQuoteStyle,
+ onRichTextTap = onRichTextTap,
+ onRichTextLongPress = onRichTextLongPress,
onOpenPhotoFullscreen = onOpenPhotoFullscreen,
onOpenVideoFullscreen = onOpenVideoFullscreen,
onOpenFileExternally = onOpenFileExternally
@@ -1145,6 +1324,9 @@ fun InstantViewBlock(
textSizeMultiplier = textSizeMultiplier,
searchQuery = searchQuery,
stateKeyPrefix = "$blockStateKey:slideshow:$index",
+ useMessageQuoteStyle = useMessageQuoteStyle,
+ onRichTextTap = onRichTextTap,
+ onRichTextLongPress = onRichTextLongPress,
onOpenPhotoFullscreen = onOpenPhotoFullscreen,
onOpenVideoFullscreen = onOpenVideoFullscreen,
onOpenFileExternally = onOpenFileExternally
@@ -1155,52 +1337,102 @@ fun InstantViewBlock(
}
is PageBlock.Table -> {
- Card(
- colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
- shape = RoundedCornerShape(12.dp)
- ) {
- Column(
+ BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
+ val tableWidth = remember(block.cells, maxWidth) {
+ resolveInstantViewTableWidth(
+ availableWidth = maxWidth,
+ columnCount = block.cells.maxOfOrNull { row ->
+ row.sumOf { it.colspan.coerceAtLeast(1) }
+ } ?: 1
+ )
+ }
+ val dividerColor = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.45f)
+ Box(
modifier = Modifier
.horizontalScroll(rememberScrollState())
- .padding(16.dp)
) {
- if (block.caption !is RichText.Plain || (block.caption as RichText.Plain).text.isNotEmpty()) {
- RichTextView(
- richText = block.caption,
- style = MaterialTheme.typography.titleSmall,
- textSizeMultiplier = textSizeMultiplier,
- modifier = Modifier.padding(bottom = 12.dp),
- color = MaterialTheme.colorScheme.onSurface
- )
- }
- block.cells.forEachIndexed { index, row ->
- Row(modifier = Modifier.fillMaxWidth()) {
- row.forEach { cell ->
- Box(
- modifier = Modifier
- .widthIn(min = 120.dp * cell.colspan)
- .border(
- width = if (block.isBordered) 1.dp else 0.dp,
- color = MaterialTheme.colorScheme.outlineVariant
- )
- .background(
- if (cell.isHeader) MaterialTheme.colorScheme.surfaceContainerHigh else Color.Transparent
+ Column(
+ modifier = Modifier
+ .width(tableWidth)
+ .clip(RoundedCornerShape(12.dp))
+ .background(MaterialTheme.colorScheme.surfaceContainerLow)
+ .padding(12.dp)
+ ) {
+ if (block.caption !is RichText.Plain || (block.caption as RichText.Plain).text.isNotEmpty()) {
+ RichTextView(
+ richText = block.caption,
+ style = MaterialTheme.typography.titleSmall,
+ textSizeMultiplier = textSizeMultiplier,
+ modifier = Modifier.padding(bottom = 10.dp),
+ color = MaterialTheme.colorScheme.onSurface,
+ onClick = onRichTextTap,
+ onLongClick = onRichTextLongPress
+ )
+ }
+ block.cells.forEachIndexed { index, row ->
+ val rowBackground = when {
+ row.any { it.isHeader } -> MaterialTheme.colorScheme.surfaceContainerHighest
+ block.isStriped && index % 2 == 1 -> MaterialTheme.colorScheme.surfaceContainer
+ else -> Color.Transparent
+ }
+ val isLastRow = index == block.cells.lastIndex
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .drawBehind {
+ if (!isLastRow) {
+ val lineWidth = 1.dp.toPx()
+ drawLine(
+ color = dividerColor,
+ start = Offset(0f, size.height - lineWidth / 2f),
+ end = Offset(
+ size.width,
+ size.height - lineWidth / 2f
+ ),
+ strokeWidth = lineWidth
+ )
+ }
+ },
+ verticalAlignment = Alignment.Top
+ ) {
+ row.forEachIndexed { cellIndex, cell ->
+ Box(
+ modifier = Modifier
+ .weight(cell.colspan.toFloat())
+ .background(rowBackground)
+ .drawBehind {
+ val lineWidth = 1.dp.toPx()
+ if (cellIndex < row.lastIndex) {
+ drawLine(
+ color = dividerColor,
+ start = Offset(
+ size.width - lineWidth / 2f,
+ 0f
+ ),
+ end = Offset(
+ size.width - lineWidth / 2f,
+ size.height
+ ),
+ strokeWidth = lineWidth
+ )
+ }
+ }
+ .padding(horizontal = 10.dp, vertical = 8.dp)
+ ) {
+ RichTextView(
+ richText = cell.text,
+ style = if (cell.isHeader) MaterialTheme.typography.labelLarge else MaterialTheme.typography.bodyMedium,
+ textSizeMultiplier = textSizeMultiplier,
+ fontWeight = if (cell.isHeader) FontWeight.Bold else FontWeight.Normal,
+ color = MaterialTheme.colorScheme.onSurface,
+ textAlign = cell.align.toTextAlign(),
+ onClick = onRichTextTap,
+ onLongClick = onRichTextLongPress
)
- .padding(8.dp)
- ) {
- RichTextView(
- richText = cell.text,
- style = if (cell.isHeader) MaterialTheme.typography.labelLarge else MaterialTheme.typography.bodyMedium,
- textSizeMultiplier = textSizeMultiplier,
- fontWeight = if (cell.isHeader) FontWeight.Bold else FontWeight.Normal,
- color = MaterialTheme.colorScheme.onSurface
- )
+ }
}
}
}
- if (index < block.cells.size - 1 && !block.isBordered) {
- HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
- }
}
}
}
@@ -1213,14 +1445,15 @@ fun InstantViewBlock(
style = MaterialTheme.typography.titleMedium,
textSizeMultiplier = textSizeMultiplier,
fontWeight = FontWeight.Bold,
- color = MaterialTheme.colorScheme.onSurface
+ color = MaterialTheme.colorScheme.onSurface,
+ onClick = onRichTextTap,
+ onLongClick = onRichTextLongPress
)
block.articles.forEach { article ->
Card(
onClick = { onUrlClick(normalizeUrl(article.url)) },
modifier = Modifier.fillMaxWidth(),
- colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLow),
- border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerHigh)
) {
ListItem(
headlineContent = {
@@ -1342,6 +1575,14 @@ private sealed interface InstantViewFullscreenMedia {
) : InstantViewFullscreenMedia
}
+private fun HorizontalAlignment.toTextAlign(): TextAlign {
+ return when (this) {
+ HorizontalAlignment.LEFT -> TextAlign.Left
+ HorizontalAlignment.CENTER -> TextAlign.Center
+ HorizontalAlignment.RIGHT -> TextAlign.Right
+ }
+}
+
@Composable
private fun InstantViewPlayablePreview(
stateKey: String,
diff --git a/presentation/src/main/java/org/monogram/presentation/features/instantview/components/InstantViewComponents.kt b/presentation/src/main/java/org/monogram/presentation/features/instantview/components/InstantViewComponents.kt
index 2c2e92b37..6affd9ac3 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/instantview/components/InstantViewComponents.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/instantview/components/InstantViewComponents.kt
@@ -23,6 +23,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.input.pointer.pointerInput
@@ -61,12 +62,28 @@ fun RichTextView(
fontStyle: FontStyle? = null,
color: Color = Color.Unspecified,
textAlign: TextAlign? = null,
- maxLines: Int = Int.MAX_VALUE
+ maxLines: Int = Int.MAX_VALUE,
+ onClick: ((Offset) -> Unit)? = null,
+ onLongClick: ((Offset) -> Unit)? = null
) {
val onUrlClick = LocalOnUrlClick.current
val linkColor = MaterialTheme.colorScheme.primary
val annotatedString = remember(richText, linkColor) { renderRichText(richText, linkColor) }
var textLayoutResult by remember { mutableStateOf(null) }
+ val hasInteractiveAnnotations = remember(annotatedString) {
+ annotatedString.getStringAnnotations(tag = "URL", start = 0, end = annotatedString.length)
+ .isNotEmpty() ||
+ annotatedString.getStringAnnotations(
+ tag = "EMAIL",
+ start = 0,
+ end = annotatedString.length
+ ).isNotEmpty() ||
+ annotatedString.getStringAnnotations(
+ tag = "PHONE",
+ start = 0,
+ end = annotatedString.length
+ ).isNotEmpty()
+ }
val scaledStyle = style.copy(
fontSize = style.fontSize * textSizeMultiplier,
@@ -80,55 +97,75 @@ fun RichTextView(
Text(
text = annotatedString,
style = scaledStyle,
- modifier = modifier.pointerInput(annotatedString, onUrlClick) {
- detectTapGestures { position ->
- val layout = textLayoutResult ?: return@detectTapGestures
- val offset = layout.getOffsetForPosition(position)
+ modifier = modifier.then(
+ if (hasInteractiveAnnotations || onClick != null || onLongClick != null) {
+ Modifier.pointerInput(annotatedString, onUrlClick, onClick, onLongClick) {
+ detectTapGestures(
+ onTap = { position ->
+ val layout = textLayoutResult ?: run {
+ onClick?.invoke(position)
+ return@detectTapGestures
+ }
+ val offset = layout.getOffsetForPosition(position)
- when {
- annotatedString.getStringAnnotations(tag = "URL", start = offset, end = offset)
- .firstOrNull() != null -> {
- val annotation = annotatedString.getStringAnnotations(
- tag = "URL",
- start = offset,
- end = offset
- )
- .first()
- onUrlClick(normalizeUrl(annotation.item))
- }
+ val handled = when {
+ annotatedString.getStringAnnotations(
+ tag = "URL",
+ start = offset,
+ end = offset
+ )
+ .firstOrNull() != null -> {
+ val annotation = annotatedString.getStringAnnotations(
+ tag = "URL",
+ start = offset,
+ end = offset
+ ).first()
+ onUrlClick(normalizeUrl(annotation.item))
+ true
+ }
- annotatedString.getStringAnnotations(
- tag = "EMAIL",
- start = offset,
- end = offset
- )
- .firstOrNull() != null -> {
- val annotation = annotatedString.getStringAnnotations(
- tag = "EMAIL",
- start = offset,
- end = offset
- )
- .first()
- onUrlClick("mailto:${annotation.item}")
- }
+ annotatedString.getStringAnnotations(
+ tag = "EMAIL",
+ start = offset,
+ end = offset
+ ).firstOrNull() != null -> {
+ val annotation = annotatedString.getStringAnnotations(
+ tag = "EMAIL",
+ start = offset,
+ end = offset
+ ).first()
+ onUrlClick("mailto:${annotation.item}")
+ true
+ }
+
+ annotatedString.getStringAnnotations(
+ tag = "PHONE",
+ start = offset,
+ end = offset
+ ).firstOrNull() != null -> {
+ val annotation = annotatedString.getStringAnnotations(
+ tag = "PHONE",
+ start = offset,
+ end = offset
+ ).first()
+ onUrlClick("tel:${annotation.item}")
+ true
+ }
- annotatedString.getStringAnnotations(
- tag = "PHONE",
- start = offset,
- end = offset
+ else -> false
+ }
+
+ if (!handled) {
+ onClick?.invoke(position)
+ }
+ },
+ onLongPress = { position -> onLongClick?.invoke(position) }
)
- .firstOrNull() != null -> {
- val annotation = annotatedString.getStringAnnotations(
- tag = "PHONE",
- start = offset,
- end = offset
- )
- .first()
- onUrlClick("tel:${annotation.item}")
- }
}
+ } else {
+ Modifier
}
- },
+ ),
maxLines = maxLines,
onTextLayout = { textLayoutResult = it }
)
diff --git a/presentation/src/main/java/org/monogram/presentation/features/instantview/components/InstantViewUtils.kt b/presentation/src/main/java/org/monogram/presentation/features/instantview/components/InstantViewUtils.kt
index 55be0be19..2877b560d 100644
--- a/presentation/src/main/java/org/monogram/presentation/features/instantview/components/InstantViewUtils.kt
+++ b/presentation/src/main/java/org/monogram/presentation/features/instantview/components/InstantViewUtils.kt
@@ -178,6 +178,18 @@ fun AnnotatedString.Builder.appendRichText(richText: RichText, linkColor: Color)
}
is RichText.Reference -> {
+ withStyle(SpanStyle(color = linkColor, textDecoration = TextDecoration.Underline)) {
+ if (richText.url.isNotBlank()) {
+ pushStringAnnotation(tag = "URL", annotation = richText.url)
+ }
+ appendRichText(richText.text, linkColor)
+ if (richText.url.isNotBlank()) {
+ pop()
+ }
+ }
+ }
+
+ is RichText.ReferenceLink -> {
pushStringAnnotation(tag = "URL", annotation = richText.url)
withStyle(SpanStyle(color = linkColor, textDecoration = TextDecoration.Underline)) {
appendRichText(richText.text, linkColor)
@@ -185,6 +197,18 @@ fun AnnotatedString.Builder.appendRichText(richText: RichText, linkColor: Color)
pop()
}
+ is RichText.Diff -> {
+ withStyle(SpanStyle(textDecoration = TextDecoration.LineThrough, color = Color.Gray)) {
+ appendRichText(richText.oldText, linkColor)
+ }
+ val newText = richTextPlainText(richText.text)
+ val oldText = richTextPlainText(richText.oldText)
+ if (oldText.isNotBlank() && newText.isNotBlank()) {
+ append(" -> ")
+ }
+ appendRichText(richText.text, linkColor)
+ }
+
is RichText.Subscript -> withStyle(SpanStyle(baselineShift = BaselineShift.Subscript)) {
appendRichText(richText.text, linkColor)
}
@@ -213,7 +237,9 @@ fun PageBlock.containsText(query: String): Boolean {
is PageBlock.Footer -> footer.containsText(query)
is PageBlock.Thinking -> text.containsText(query)
is PageBlock.MathematicalExpression -> expression.contains(query, ignoreCase = true)
- is PageBlock.BlockQuote -> text.containsText(query) || credit.containsText(query)
+ is PageBlock.BlockQuote -> pageBlocks.any { it.containsText(query) } || credit.containsText(
+ query
+ )
is PageBlock.PullQuote -> text.containsText(query) || credit.containsText(query)
is PageBlock.ListBlock -> items.any { item ->
item.label.contains(
@@ -250,6 +276,9 @@ fun PageBlock.containsText(query: String): Boolean {
is PageBlock.Anchor -> name.contains(query, ignoreCase = true)
is PageBlock.AudioBlock -> caption.text.containsText(query) || caption.credit.containsText(query)
+ is PageBlock.VoiceNoteBlock -> caption.text.containsText(query) || caption.credit.containsText(
+ query
+ )
is PageBlock.Cover -> cover.containsText(query)
PageBlock.Divider -> false
is PageBlock.Embedded -> caption.text.containsText(query) || caption.credit.containsText(query) || url.contains(
@@ -319,6 +348,12 @@ fun RichText.containsText(query: String): Boolean {
query,
ignoreCase = true
) || url.contains(query, ignoreCase = true)
+ is RichText.ReferenceLink -> text.containsText(query) || referenceName.contains(
+ query,
+ ignoreCase = true
+ ) || url.contains(query, ignoreCase = true)
+
+ is RichText.Diff -> text.containsText(query) || oldText.containsText(query)
is RichText.Subscript -> text.containsText(query)
is RichText.Superscript -> text.containsText(query)
diff --git a/presentation/src/main/res/values/string.xml b/presentation/src/main/res/values/string.xml
index e2ce565dd..080cad177 100644
--- a/presentation/src/main/res/values/string.xml
+++ b/presentation/src/main/res/values/string.xml
@@ -1557,8 +1557,8 @@
Apply
Done
Refresh
- Fullscreen editor
- Editor
+ Open fullscreen
+ Compose
Send silently
Schedule message
Scheduled messages
@@ -1665,6 +1665,20 @@
Edit
Markdown: on
Markdown: off
+ Plain
+ Markdown
+ HTML
+ Failed to parse markup
+ HTML pre
+ LaTeX
+ Block LaTeX
+ Heading 1
+ Heading 2
+ Heading 3
+ Bulleted list
+ Numbered list
+ Divider
+ Table
A+
A-
Snippets
@@ -1683,9 +1697,11 @@
Cut
Paste
AI
- AI editor
+ AI tools
+ Translate, rewrite, or fix text
Translate
Stylize
+ Generate
Fix
Original
Result
@@ -1693,7 +1709,9 @@
Apply result
Translate text
Apply style
+ Generate text
Fix text
+ Prompt
Target language
Select language
No languages found
@@ -1707,8 +1725,10 @@
Add emojis
Processing...
Enter text to use AI
+ Enter a prompt to generate text
Too many AI requests. Telegram Premium may be required.
AI processing failed
+ Describe what to generate…
Insert
Prev
Next
@@ -1721,6 +1741,7 @@
Spoiler
Code
Monospace
+ Quote
Link
Mention
Emoji
@@ -2497,6 +2518,7 @@
Play Video
Play Animation
Audio
+ Voice note
Unknown Artist
Play
Open
diff --git a/presentation/src/test/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/EditorRichTextParsingTest.kt b/presentation/src/test/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/EditorRichTextParsingTest.kt
new file mode 100644
index 000000000..c15b850d2
--- /dev/null
+++ b/presentation/src/test/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/EditorRichTextParsingTest.kt
@@ -0,0 +1,59 @@
+package org.monogram.presentation.features.chats.conversation.ui.inputbar
+
+import androidx.compose.ui.text.AnnotatedString
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.monogram.domain.models.MessageEntityType
+import org.monogram.presentation.features.chats.conversation.ui.message.model.entitiesForBlock
+import org.monogram.presentation.features.chats.conversation.ui.message.model.topLevelBlockEntities
+
+class EditorRichTextParsingTest {
+ @Test
+ fun `markdown quote keeps nested table content`() {
+ val input = AnnotatedString(
+ "> A | B\n> --- | ---\n> 1 | 2\n> 3 | 4"
+ )
+
+ val parsed = MarkdownRichTextParser(input).parse()
+
+ assertTrue(parsed.text.contains("┌"))
+ assertTrue(parsed.text.contains("1"))
+ assertTrue(parsed.text.contains("4"))
+ assertFalse(parsed.text.contains(">"))
+ }
+
+ @Test
+ fun `markdown quote table is extracted as nested table block`() {
+ val input = AnnotatedString(
+ "> | A | B |\n> | --- | --- |\n> | 1 | 2 |"
+ )
+
+ val parsed = MarkdownRichTextParser(input).parse()
+ val entities = extractEntities(parsed, emptyMap())
+
+ assertTrue(parsed.text.contains("┌"))
+ assertTrue(parsed.text.contains("│ A │ B │"))
+ assertEquals(1, entities.count { it.type is MessageEntityType.BlockQuote })
+ assertEquals(1, entities.count { (it.type as? MessageEntityType.Pre)?.language == "table" })
+ }
+
+ @Test
+ fun `markdown quote table keeps table nested under top level quote block`() {
+ val input = AnnotatedString(
+ "> | A | B |\n> | --- | --- |\n> | 1 | 2 |"
+ )
+
+ val parsed = MarkdownRichTextParser(input).parse()
+ val entities = extractEntities(parsed, emptyMap())
+ val topLevelBlock = entities.topLevelBlockEntities().single()
+
+ assertTrue(topLevelBlock.type is MessageEntityType.BlockQuote)
+ assertEquals(
+ 1,
+ entities.entitiesForBlock(topLevelBlock)
+ .count { (it.type as? MessageEntityType.Pre)?.language == "table" }
+ )
+ }
+}