compose: implement BBcode and HTML sending

main
Alibek Omarov 4 years ago
parent 569ca90147
commit ca196ffc73
  1. 753
      app/schemas/com.keylesspalace.tusky.db.AppDatabase/23.json
  2. 2
      app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java
  3. 124
      app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt
  4. 9
      app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt
  5. 3
      app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt
  6. 12
      app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java
  7. 17
      app/src/main/java/com/keylesspalace/tusky/db/TootEntity.java
  8. 3
      app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt
  9. 2
      app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt
  10. 36
      app/src/main/java/com/keylesspalace/tusky/fragment/preference/AccountPreferencesFragment.kt
  11. 2
      app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt
  12. 9
      app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt
  13. 4
      app/src/main/java/com/keylesspalace/tusky/util/SaveTootHelper.java
  14. 18
      app/src/main/res/drawable/ic_bbcode_24dp.xml
  15. 18
      app/src/main/res/drawable/ic_html_24dp.xml
  16. 2
      app/src/main/res/layout/activity_compose.xml
  17. 4
      app/src/main/res/values/husky.xml
  18. 13
      app/src/main/res/values/husky_donottranslate.xml
  19. 8
      app/src/main/res/xml/account_preferences.xml

@ -0,0 +1,753 @@
{
"formatVersion": 1,
"database": {
"version": 23,
"identityHash": "0cb482507cdcf5628ae028242c3b74bb",
"entities": [
{
"tableName": "TootEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER, `poll` TEXT, `formattingSyntax` TEXT NOT NULL, `markdownMode` INTEGER)",
"fields": [
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "urls",
"columnName": "urls",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "descriptions",
"columnName": "descriptions",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "contentWarning",
"columnName": "contentWarning",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inReplyToId",
"columnName": "inReplyToId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inReplyToText",
"columnName": "inReplyToText",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inReplyToUsername",
"columnName": "inReplyToUsername",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "visibility",
"columnName": "visibility",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "poll",
"columnName": "poll",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "formattingSyntax",
"columnName": "formattingSyntax",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "markdownMode",
"columnName": "markdownMode",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"uid"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "AccountEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsEmojiReactions` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `defaultFormattingSyntax` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "domain",
"columnName": "domain",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accessToken",
"columnName": "accessToken",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isActive",
"columnName": "isActive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "profilePictureUrl",
"columnName": "profilePictureUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "notificationsEnabled",
"columnName": "notificationsEnabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsMentioned",
"columnName": "notificationsMentioned",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsFollowed",
"columnName": "notificationsFollowed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsReblogged",
"columnName": "notificationsReblogged",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsFavorited",
"columnName": "notificationsFavorited",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsPolls",
"columnName": "notificationsPolls",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsEmojiReactions",
"columnName": "notificationsEmojiReactions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationSound",
"columnName": "notificationSound",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationVibration",
"columnName": "notificationVibration",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationLight",
"columnName": "notificationLight",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "defaultPostPrivacy",
"columnName": "defaultPostPrivacy",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "defaultMediaSensitivity",
"columnName": "defaultMediaSensitivity",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "alwaysShowSensitiveMedia",
"columnName": "alwaysShowSensitiveMedia",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "alwaysOpenSpoiler",
"columnName": "alwaysOpenSpoiler",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mediaPreviewEnabled",
"columnName": "mediaPreviewEnabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastNotificationId",
"columnName": "lastNotificationId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "activeNotifications",
"columnName": "activeNotifications",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "emojis",
"columnName": "emojis",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tabPreferences",
"columnName": "tabPreferences",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "notificationsFilter",
"columnName": "notificationsFilter",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "defaultFormattingSyntax",
"columnName": "defaultFormattingSyntax",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_AccountEntity_domain_accountId",
"unique": true,
"columnNames": [
"domain",
"accountId"
],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)"
}
],
"foreignKeys": []
},
{
"tableName": "InstanceEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))",
"fields": [
{
"fieldPath": "instance",
"columnName": "instance",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "emojiList",
"columnName": "emojiList",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "maximumTootCharacters",
"columnName": "maximumTootCharacters",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxPollOptions",
"columnName": "maxPollOptions",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxPollOptionLength",
"columnName": "maxPollOptionLength",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "version",
"columnName": "version",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"instance"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TimelineStatusEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "timelineUserId",
"columnName": "timelineUserId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "authorServerId",
"columnName": "authorServerId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inReplyToId",
"columnName": "inReplyToId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inReplyToAccountId",
"columnName": "inReplyToAccountId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "createdAt",
"columnName": "createdAt",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "emojis",
"columnName": "emojis",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reblogsCount",
"columnName": "reblogsCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "favouritesCount",
"columnName": "favouritesCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "reblogged",
"columnName": "reblogged",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bookmarked",
"columnName": "bookmarked",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "favourited",
"columnName": "favourited",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sensitive",
"columnName": "sensitive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "spoilerText",
"columnName": "spoilerText",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "visibility",
"columnName": "visibility",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "attachments",
"columnName": "attachments",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "mentions",
"columnName": "mentions",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "application",
"columnName": "application",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reblogServerId",
"columnName": "reblogServerId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reblogAccountId",
"columnName": "reblogAccountId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "poll",
"columnName": "poll",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"serverId",
"timelineUserId"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_TimelineStatusEntity_authorServerId_timelineUserId",
"unique": false,
"columnNames": [
"authorServerId",
"timelineUserId"
],
"createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)"
}
],
"foreignKeys": [
{
"table": "TimelineAccountEntity",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"authorServerId",
"timelineUserId"
],
"referencedColumns": [
"serverId",
"timelineUserId"
]
}
]
},
{
"tableName": "TimelineAccountEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))",
"fields": [
{
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timelineUserId",
"columnName": "timelineUserId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "localUsername",
"columnName": "localUsername",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "avatar",
"columnName": "avatar",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "emojis",
"columnName": "emojis",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "bot",
"columnName": "bot",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"serverId",
"timelineUserId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "ConversationEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))",
"fields": [
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accounts",
"columnName": "accounts",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.id",
"columnName": "s_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastStatus.url",
"columnName": "s_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastStatus.inReplyToId",
"columnName": "s_inReplyToId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastStatus.inReplyToAccountId",
"columnName": "s_inReplyToAccountId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastStatus.account",
"columnName": "s_account",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastStatus.content",
"columnName": "s_content",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastStatus.createdAt",
"columnName": "s_createdAt",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.emojis",
"columnName": "s_emojis",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastStatus.favouritesCount",
"columnName": "s_favouritesCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.favourited",
"columnName": "s_favourited",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.bookmarked",
"columnName": "s_bookmarked",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.sensitive",
"columnName": "s_sensitive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.spoilerText",
"columnName": "s_spoilerText",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastStatus.attachments",
"columnName": "s_attachments",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastStatus.mentions",
"columnName": "s_mentions",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastStatus.showingHiddenContent",
"columnName": "s_showingHiddenContent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.expanded",
"columnName": "s_expanded",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.collapsible",
"columnName": "s_collapsible",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.collapsed",
"columnName": "s_collapsed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.poll",
"columnName": "s_poll",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id",
"accountId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0cb482507cdcf5628ae028242c3b74bb')"
]
}
}

@ -179,7 +179,7 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd
/*scheduledAt*/null,
/*sensitive*/null,
/*poll*/null,
item.getMarkdownMode()
item.getFormattingSyntax()
);
Intent intent = ComposeActivity.startIntent(this, composeOptions);
startActivity(intent);

@ -113,6 +113,7 @@ class ComposeActivity : BaseActivity(),
private var composeOptions: ComposeOptions? = null
private val viewModel: ComposeViewModel by viewModels { viewModelFactory }
private var suggestFormattingSyntax: String = "text/markdown"
private var mediaCount = 0
@ -148,6 +149,7 @@ class ComposeActivity : BaseActivity(),
setupButtons()
photoUploadUri = savedInstanceState?.getParcelable(PHOTO_UPLOAD_URI_KEY)
viewModel.formattingSyntax = activeAccount.defaultFormattingSyntax
/* If the composer is started up as a reply to another post, override the "starting" state
* based on what the intent from the reply request passes. */
@ -159,9 +161,14 @@ class ComposeActivity : BaseActivity(),
if (!tootText.isNullOrEmpty()) {
composeEditField.setText(tootText)
}
enableMarkdownMode(composeOptions?.markdownMode ?: false);
}
if(viewModel.formattingSyntax.length == 0) {
suggestFormattingSyntax = "text/markdown"
} else {
suggestFormattingSyntax = viewModel.formattingSyntax
}
if (!TextUtils.isEmpty(composeOptions?.scheduledAt)) {
composeScheduleView.setDateTime(composeOptions?.scheduledAt)
}
@ -326,6 +333,8 @@ class ComposeActivity : BaseActivity(),
enableButton(composeAddMediaButton, true, true)
enablePollButton(viewModel.poll != null)
}
private var supportedFormattingSyntax = arrayListOf<String>()
private fun subscribeToUpdates(mediaAdapter: MediaPreviewAdapter) {
withLifecycleContext {
@ -335,7 +344,32 @@ class ComposeActivity : BaseActivity(),
composeScheduleButton.visible(instanceData.supportsScheduled)
}
viewModel.instanceMetadata.observe { instanceData ->
composeMarkdownButton.visible(instanceData.supportsMarkdown)
if(instanceData.supportsMarkdown) {
supportedFormattingSyntax.add("text/markdown")
}
if(instanceData.supportsBBcode) {
supportedFormattingSyntax.add("text/bbcode")
}
if(instanceData.supportsHTML) {
supportedFormattingSyntax.add("text/html")
}
if(supportedFormattingSyntax.size != 0) {
composeFormattingSyntax.visible(true)
val supportsPrefferedSyntax = supportedFormattingSyntax.contains(viewModel.formattingSyntax)
if(!supportsPrefferedSyntax) {
viewModel.formattingSyntax = ""
setIconForSyntax(supportedFormattingSyntax[0], false)
} else {
setIconForSyntax(viewModel.formattingSyntax, true)
}
}
if(instanceData.software.equals("pleroma")) {
reenableAttachments()
}
@ -406,7 +440,8 @@ class ComposeActivity : BaseActivity(),
composeHideMediaButton.setOnClickListener { toggleHideMedia() }
composeScheduleButton.setOnClickListener { onScheduleClick() }
composeScheduleView.setResetOnClickListener { resetSchedule() }
composeMarkdownButton.setOnClickListener { toggleMarkdownMode() }
composeFormattingSyntax.setOnClickListener { toggleFormattingMode() }
composeFormattingSyntax.setOnLongClickListener { selectFormattingSyntax() }
atButton.setOnClickListener { atButtonClicked() }
hashButton.setOnClickListener { hashButtonClicked() }
codeButton.setOnClickListener { codeButtonClicked() }
@ -474,19 +509,80 @@ class ComposeActivity : BaseActivity(),
composeEditField.setSelection(start + text.length)
}
private fun enableMarkdownMode(enable: Boolean) {
viewModel.markdownMode = enable
private fun enableFormattingSyntaxButton(syntax: String, enable: Boolean) {
val stringId = when(syntax) {
"text/html" -> R.string.action_html
"text/bbcode" -> R.string.action_bbcode
else -> R.string.action_markdown
}
val actionStringId = if(enable) R.string.action_disable_formatting_syntax else R.string.action_enable_formatting_syntax
val tooltipText = getString(actionStringId).format(stringId)
composeFormattingSyntax.tooltipText = tooltipText
composeFormattingSyntax.contentDescription = tooltipText
enableMarkdownWYSIWYGButtons(viewModel.markdownMode)
@ColorInt val color = ThemeUtils.getColor(this, if(enable) R.attr.colorPrimary else android.R.attr.textColorTertiary);
composeFormattingSyntax.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN);
}
private fun setIconForSyntax(syntax: String, enable: Boolean) {
val drawableId = when(syntax) {
"text/html" -> R.drawable.ic_html_24dp
"text/bbcode" -> R.drawable.ic_bbcode_24dp
else -> R.drawable.ic_markdown
}
TransitionManager.beginDelayedTransition(composeMarkdownButton.parent as ViewGroup);
suggestFormattingSyntax = if(drawableId == R.drawable.ic_markdown) "text/markdown" else syntax
composeFormattingSyntax.setImageResource(drawableId)
enableFormattingSyntaxButton(syntax, enable)
}
@ColorInt val color = ThemeUtils.getColor(this, if(viewModel.markdownMode) R.attr.colorPrimary else android.R.attr.textColorTertiary);
composeMarkdownButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN);
private fun toggleFormattingMode() {
if(viewModel.formattingSyntax.equals(suggestFormattingSyntax)) {
viewModel.formattingSyntax = ""
enableFormattingSyntaxButton(suggestFormattingSyntax, false)
} else {
viewModel.formattingSyntax = suggestFormattingSyntax
enableFormattingSyntaxButton(suggestFormattingSyntax, true)
}
}
private fun toggleMarkdownMode() {
enableMarkdownMode(!viewModel.markdownMode)
private fun selectFormattingSyntax() : Boolean {
val menu = PopupMenu(this, composeFormattingSyntax)
val plaintextId = 0
val markdownId = 1
val bbcodeId = 2
val htmlId = 3
menu.menu.add(0, plaintextId, 0, R.string.action_plaintext)
if(viewModel.instanceMetadata.value?.supportsMarkdown ?: false)
menu.menu.add(0, markdownId, 0, R.string.action_markdown)
if(viewModel.instanceMetadata.value?.supportsBBcode ?: false)
menu.menu.add(0, bbcodeId, 0, R.string.action_bbcode)
if(viewModel.instanceMetadata.value?.supportsHTML ?: false)
menu.menu.add(0, htmlId, 0, R.string.action_html)
menu.setOnMenuItemClickListener { menuItem ->
val choose = when (menuItem.itemId) {
markdownId -> "text/markdown"
bbcodeId -> "text/bbcode"
htmlId -> "text/html"
else -> ""
}
if(choose.length == 0) {
// leave previous
setIconForSyntax(viewModel.formattingSyntax, false)
} else {
setIconForSyntax(choose, true)
}
viewModel.formattingSyntax = choose
true
}
menu.show()
return true
}
private fun enableMarkdownWYSIWYGButtons(visible: Boolean) {
@ -622,7 +718,7 @@ class ComposeActivity : BaseActivity(),
composeEmojiButton.isClickable = enable
composeHideMediaButton.isClickable = enable
composeScheduleButton.isClickable = enable
composeMarkdownButton.isClickable = enable
composeFormattingSyntax.isClickable = enable
composeTootButton.isEnabled = enable
}
@ -1118,7 +1214,7 @@ class ComposeActivity : BaseActivity(),
var scheduledAt: String? = null,
var sensitive: Boolean? = null,
var poll: NewPoll? = null,
var markdownMode: Boolean? = null
var formattingSyntax: String? = null
) : Parcelable
companion object {

@ -63,7 +63,7 @@ class ComposeViewModel
private var startingVisibility: Status.Visibility = Status.Visibility.UNKNOWN
private val instance: MutableLiveData<InstanceEntity?> = MutableLiveData(null)
private val nodeinfo: MutableLiveData<NodeInfo?> = MutableLiveData(null)
public var markdownMode: Boolean = false
public var formattingSyntax: String = ""
public var hasNoAttachmentLimits = false
val instanceParams: LiveData<ComposeInstanceParams> = instance.map { instance ->
@ -280,7 +280,7 @@ class ComposeViewModel
replyingStatusAuthor,
statusVisibility.value!!,
poll.value,
markdownMode
formattingSyntax
)
}
@ -318,7 +318,7 @@ class ComposeViewModel
poll = poll.value,
replyingStatusContent = null,
replyingStatusAuthorUsername = null,
markdownMode = markdownMode,
formattingSyntax = formattingSyntax,
savedJsonUrls = null,
accountId = accountManager.activeAccount!!.id,
savedTootUid = 0,
@ -487,6 +487,9 @@ class ComposeViewModel
}
replyingStatusContent = composeOptions?.replyingStatusContent
replyingStatusAuthor = composeOptions?.replyingStatusAuthor
if(composeOptions?.formattingSyntax != null)
formattingSyntax = composeOptions?.formattingSyntax ?: accountManager.activeAccount!!.defaultFormattingSyntax
}
fun updatePoll(newPoll: NewPoll) {

@ -55,7 +55,8 @@ data class AccountEntity(@field:PrimaryKey(autoGenerate = true) var id: Long,
var activeNotifications: String = "[]",
var emojis: List<Emoji> = emptyList(),
var tabPreferences: List<TabData> = defaultTabs(),
var notificationsFilter: String = "[]") {
var notificationsFilter: String = "[]",
var defaultFormattingSyntax: String = "") {
val identifier: String
get() = "$domain:$accountId"

@ -30,7 +30,7 @@ import androidx.annotation.NonNull;
@Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
TimelineAccountEntity.class, ConversationEntity.class
}, version = 22)
}, version = 23)
public abstract class AppDatabase extends RoomDatabase {
public abstract TootDao tootDao();
@ -333,4 +333,14 @@ public abstract class AppDatabase extends RoomDatabase {
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsEmojiReactions` INTEGER NOT NULL DEFAULT 1");
}
};
public static final Migration MIGRATION_22_23 = new Migration(22, 23) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
// leave markdownMode unused, we don't need it anymore but don't recreate table
// database.execSQL("ALTER TABLE `TootEntity` DROP COLUMN `markdownMode`");
database.execSQL("ALTER TABLE `TootEntity` ADD COLUMN `formattingSyntax` TEXT NOT NULL DEFAULT ''");
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `defaultFormattingSyntax` TEXT NOT NULL DEFAULT ''");
}
};
}

@ -19,7 +19,7 @@ import com.google.gson.Gson;
import com.keylesspalace.tusky.entity.NewPoll;
import com.keylesspalace.tusky.entity.Status;
import androidx.annotation.Nullable;
import androidx.annotation.*;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
@ -66,13 +66,18 @@ public class TootEntity {
@ColumnInfo(name = "poll")
private final NewPoll poll;
@NonNull
@ColumnInfo(name = "formattingSyntax")
private final String formattingSyntax;
/* DEPRECATED */
@Nullable
@ColumnInfo(name = "markdownMode")
private final Boolean markdownMode;
public Boolean markdownMode = false;
public TootEntity(int uid, String text, String urls, String descriptions, String contentWarning, String inReplyToId,
@Nullable String inReplyToText, @Nullable String inReplyToUsername,
Status.Visibility visibility, @Nullable NewPoll poll, @Nullable Boolean markdownMode) {
Status.Visibility visibility, @Nullable NewPoll poll, String formattingSyntax) {
this.uid = uid;
this.text = text;
this.urls = urls;
@ -83,7 +88,7 @@ public class TootEntity {
this.inReplyToUsername = inReplyToUsername;
this.visibility = visibility;
this.poll = poll;
this.markdownMode = markdownMode;
this.formattingSyntax = formattingSyntax;
}
public String getText() {
@ -129,6 +134,10 @@ public class TootEntity {
return poll;
}
public String getFormattingSyntax() {
return formattingSyntax;
}
@Nullable
public Boolean getMarkdownMode() {
return markdownMode;

@ -79,7 +79,8 @@ class AppModule {
AppDatabase.MIGRATION_11_12, AppDatabase.MIGRATION_12_13, AppDatabase.MIGRATION_10_13,
AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16,
AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19,
AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22)
AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22,
AppDatabase.MIGRATION_22_23)
.build()
}

@ -59,7 +59,7 @@ class NetworkModule {
.apply {
addInterceptor(InstanceSwitchAuthInterceptor(accountManager))
if (BuildConfig.DEBUG) {
addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BASIC })
addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.HEADERS })
}
}
.build()

@ -65,6 +65,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
private lateinit var defaultPostPrivacyPreference: ListPreference
private lateinit var defaultMediaSensitivityPreference: SwitchPreferenceCompat
private lateinit var defaultFormattingSyntaxPreference: ListPreference
private lateinit var alwaysShowSensitiveMediaPreference: SwitchPreferenceCompat
private lateinit var alwaysOpenSpoilerPreference: SwitchPreferenceCompat
private lateinit var mediaPreviewEnabledPreference: SwitchPreferenceCompat
@ -85,6 +86,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
mutedDomainsPreference = requirePreference("mutedDomainsPreference")
defaultPostPrivacyPreference = requirePreference("defaultPostPrivacy") as ListPreference
defaultMediaSensitivityPreference = requirePreference("defaultMediaSensitivity") as SwitchPreferenceCompat
defaultFormattingSyntaxPreference = requirePreference("defaultFormattingSyntax") as ListPreference
mediaPreviewEnabledPreference = requirePreference("mediaPreviewEnabled") as SwitchPreferenceCompat
alwaysShowSensitiveMediaPreference = requirePreference("alwaysShowSensitiveMedia") as SwitchPreferenceCompat
alwaysOpenSpoilerPreference = requirePreference("alwaysOpenSpoiler") as SwitchPreferenceCompat
@ -110,6 +112,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
defaultPostPrivacyPreference.onPreferenceChangeListener = this
defaultMediaSensitivityPreference.onPreferenceChangeListener = this
defaultFormattingSyntaxPreference.onPreferenceChangeListener = this
mediaPreviewEnabledPreference.onPreferenceChangeListener = this
alwaysShowSensitiveMediaPreference.onPreferenceChangeListener = this
alwaysOpenSpoilerPreference.onPreferenceChangeListener = this
@ -122,6 +125,14 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
defaultPostPrivacyPreference.value = it.defaultPostPrivacy.serverString()
defaultPostPrivacyPreference.icon = getIconForVisibility(it.defaultPostPrivacy)
defaultFormattingSyntaxPreference.value = when(it.defaultFormattingSyntax) {
"text/markdown" -> "Markdown"
"text/bbcode" -> "BBCode"
"text/html" -> "HTML"
else -> "Plaintext"
}
defaultFormattingSyntaxPreference.icon = getIconForSyntax(it.defaultFormattingSyntax)
defaultMediaSensitivityPreference.isChecked = it.defaultMediaSensitivity
defaultMediaSensitivityPreference.icon = getIconForSensitivity(it.defaultMediaSensitivity)
@ -143,6 +154,19 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
preference.icon = getIconForSensitivity(newValue as Boolean)
syncWithServer(sensitive = newValue)
}
defaultFormattingSyntaxPreference -> {
val syntax = when(newValue) {
"Markdown" -> "text/markdown"
"BBCode" -> "text/bbcode"
"HTML" -> "text/html"
else -> ""
}
preference.icon = getIconForSyntax(syntax)
accountManager.activeAccount?.let {
it.defaultFormattingSyntax = syntax
accountManager.saveAccount(it)
}
}
mediaPreviewEnabledPreference -> {
accountManager.activeAccount?.let {
it.mediaPreviewEnabled = newValue as Boolean
@ -287,8 +311,20 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
return getTintedIcon(drawableId)
}
private fun getIconForSyntax(syntax: String): Drawable? {
val drawableId = when(syntax) {
"text/html" -> R.drawable.ic_html_24dp
"text/bbcode" -> R.drawable.ic_bbcode_24dp
"text/markdown" -> R.drawable.ic_markdown
else -> 0
}
return getTintedIcon(drawableId)
}
private fun getTintedIcon(iconId: Int): Drawable? {
if(iconId == 0) return null
return ThemeUtils.getTintedDrawable(requireContext(), iconId, R.attr.iconColor)
}

@ -102,7 +102,7 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
null,
null,
null,
null,
"",
account.id,
0,
randomAlphanumericString(16),

@ -130,8 +130,8 @@ class SendTootService : Service(), Injectable {
}
tootToSend.retries++
val contentType : String? = if (tootToSend.markdownMode != null && tootToSend.markdownMode == true) "text/markdown" else null
val contentType : String? = if(tootToSend.formattingSyntax.length == 0) null else tootToSend.formattingSyntax
val newStatus = NewStatus(
tootToSend.text,
@ -152,7 +152,6 @@ class SendTootService : Service(), Injectable {
newStatus
)
sendCalls[tootId] = sendCall
val callback = object : Callback<Status> {
@ -259,7 +258,7 @@ class SendTootService : Service(), Injectable {
toot.replyingStatusAuthorUsername,
Status.Visibility.byString(toot.visibility),
toot.poll,
toot.markdownMode)
toot.formattingSyntax)
}
private fun cancelSendingIntent(tootId: Int): PendingIntent {
@ -328,7 +327,7 @@ data class TootToSend(
val replyingStatusContent: String?,
val replyingStatusAuthorUsername: String?,
val savedJsonUrls: List<String>?,
val markdownMode: Boolean?,
val formattingSyntax: String,
val accountId: Long,
val savedTootUid: Int,
val idempotencyKey: String,

@ -57,7 +57,7 @@ public final class SaveTootHelper {
@Nullable String replyingStatusAuthorUsername,
@NonNull Status.Visibility statusVisibility,
@Nullable NewPoll poll,
@Nullable Boolean markdownMode) {
@NonNull String formattingSyntax) {
if (TextUtils.isEmpty(content) && mediaUris.isEmpty() && poll == null) {
return false;
@ -89,7 +89,7 @@ public final class SaveTootHelper {
replyingStatusContent,
replyingStatusAuthorUsername,
statusVisibility,
poll, markdownMode);
poll, formattingSyntax);
new AsyncTask<Void, Void, Void>() {
@Override

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
android:viewportWidth="24"
android:viewportHeight="24"
android:width="24dp"
android:height="24dp">
<group
android:translateY="-273">
<path
android:pathData="M5.4784346 278.1925v1.04731H4.1624082v11.5204h1.3160264v1.04731H2.8877229V278.1925Z"
android:fillColor="#000000" />
<path
android:pathData="M14.532145 279.62566L10.3498 290.51905H9.2542592l4.1892358 -10.89339z"
android:fillColor="#000000" />
<path
android:pathData="M18.507785 279.23981v-1.04731h2.604492v13.61502h-2.604492v-1.04731h1.322917v-11.5204z"
android:fillColor="#000000" />
</group>
</vector>

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
android:viewportWidth="24"
android:viewportHeight="24"
android:width="24dp"
android:height="24dp">
<group
android:translateY="-273">
<path
android:pathData="M7.9864639 288.23494l-5.636176 -2.61138v-0.99908l5.636176 -2.60449v1.35048l-4.299479 1.77078 4.299479 1.74321z"
android:fillColor="#000000" />
<path
android:pathData="M14.428792 279.5533l-4.182346 10.89339h-1.09554l4.189236 -10.89339z"
android:fillColor="#000000" />
<path
android:pathData="M15.765489 282.00621l5.884223 2.60449v1.00597l-5.884223 2.61138v-1.31603l4.561306 -1.81212 -4.561306 -1.77766z"
android:fillColor="#000000" />
</group>
</vector>

@ -375,7 +375,7 @@
app:srcCompat="@drawable/ic_access_time" />
<ImageButton
android:id="@+id/composeMarkdownButton"
android:id="@+id/composeFormattingSyntax"
style="@style/TuskyImageButton"
android:layout_width="36dp"
android:layout_height="36dp"

@ -1,12 +1,13 @@
<resources>
<string name="action_reply_to">Reply to</string>
<string name="action_markdown">Markdown</string>
<string name="action_mute_conversation">Mute conversation</string>
<string name="action_unmute_conversation">Unmute conversation</string>
<string name="action_emoji_react">React</string>
<string name="action_emoji_unreact">Remove reaction</string>
<string name="action_emoji_reacted_by">Who reacted</string>
<string name="action_enable_formatting_syntax">Enable %s</string>
<string name="action_disable_formatting_syntax">Disable %s</string>
<string name="title_emoji_reacted_by">%s reacted by</string>
@ -22,6 +23,7 @@
<string name="notification_emoji_name">Emoji Reactions</string>
<string name="notification_emoji_description">Notifications about new emoji reactions</string>
<string name="pref_title_default_formatting">Default formatting syntax(if supported by instance)</string>
<string name="pref_title_notification_filter_emoji">my posts are reacted with emojis</string>
<string name="pref_title_hide_muted_users">Hide muted users</string>
</resources>

@ -0,0 +1,13 @@
<resources>
<string name="action_plaintext">Plaintext</string>
<string name="action_markdown">Markdown</string>
<string name="action_bbcode">BBCode</string>
<string name="action_html">HTML</string>
<string-array name="formatting_syntax_values">
<item>@string/action_plaintext</item>
<item>@string/action_markdown</item>
<item>@string/action_bbcode</item>
<item>@string/action_html</item>
</string-array>
</resources>

@ -31,6 +31,14 @@
android:key="defaultPostPrivacy"
android:summary="%s"
android:title="@string/pref_default_post_privacy" />
<ListPreference
android:defaultValue="Plaintext"
android:entries="@array/formatting_syntax_values"
android:entryValues="@array/formatting_syntax_values"
android:key="defaultFormattingSyntax"
android:summary="%s"
android:title="@string/pref_title_default_formatting" />
<SwitchPreferenceCompat
android:defaultValue="false"

Loading…
Cancel
Save