diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 000000000..e67c4932f
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "localization"]
+ path = localization
+ url = git@github.com:TwidereProject/TwidereX-Localization.git
diff --git a/README.md b/README.md
index 5c4b0a2af..03f8c0ded 100644
--- a/README.md
+++ b/README.md
@@ -23,30 +23,23 @@ Next generation of Twidere for Android 5.0+. **Still in early stage.**
## What's Happening
-### What's new in 1.4.0 - Jul 2021 Update
-
-- Twitter DM (Direct Message) support is here!
-- Now you can use Fido key and password manager when login.
-- Add notification setting in the application.
-- Add pure dark mode support when dark theme is selected, which is really black.
-- Fix nitter usage for viewing private tweets, now you can view private tweet thread when using nitter.
-- Fix some really old mention is being notified when using Twitter.
-- Fix when the TopBar BottomBar and FloatingButton sometimes in the intermediate state [#152](https://github.com/TwidereProject/TwidereX-Android/pull/152) by [HuixingWong](https://github.com/HuixingWong)
-- Fix the status gap algorithm, now the gap is more accurate than before.
-- Fix VideoPlayer is still playing when the app is in background [#173](https://github.com/TwidereProject/TwidereX-Android/pull/173) by [HuixingWong](https://github.com/HuixingWong)
-- Rework for text input, now the beginning word in sentences is auto-capitalized when compose.
-- Fix notification timestamp not being used, the can fix the issue where old notification still being notified.
-- Fix crashing when the user cancels adding the member to the list.
-- Fix timeline not able to refresh after changing account.
-- Fix clicking on notification opens blank app.
-- Upgrade Jetpack Compose to RC02.
-
-### What is being planned for 1.5.0 - Jul 2021 Update
-For 1.5.0, as we've finished the basic functionality for Twitter and Mastodon, we're now focusing on the functionality that is lacking in Twidere X, such as Tabs editing, proxy support, and UX improvement, you can check out our [milestore](https://github.com/TwidereProject/TwidereX-Android/milestone/3) for detail. Here is a shortlist:
-
-- Proxy support.
-- Tabs editing support.
-- Internal changes that preparing for the desktop version.
+### What's new in 1.5.0 - Aug 2021 Update
+
+- Proxy support, you can set a proxy for all the network request in settings.
+- Tabs column editing support, you can now modify the order and count of the home tab.
+- "Tweet sent" notification will be dismissed after showing once.
+- Screen will keep on when playing media in media scene.
+- Better RTL support for tweets.
+- Fix certain crashes when requesting network.
+- [Mastodon] Add support for gif avatar support and custom emoji in user name.
+- [Mastodon] Add federated timeline and local timeline
+
+### What is being planned for 1.6.0 - Aug 2021 Update
+For 1.6.0, we're planning to build an experimatal desktop version, this is a big step, you can check out our [milestore](https://github.com/TwidereProject/TwidereX-Android/milestone/4) for detail. Here is a shortlist:
+
+- Experimatal desktop version.
+- Mute and block support.
+- Optimizing video play for timelien.
- Bug fixes.
- UI/UX tweaking.
- Stability.
diff --git a/app/build.gradle b/app/build.gradle
deleted file mode 100644
index 907aa5b0e..000000000
--- a/app/build.gradle
+++ /dev/null
@@ -1,229 +0,0 @@
-buildscript {
- ext {
- enableGoogleVariant = project.file("google-services.json").exists()
- }
- repositories {
- google()
- }
-
- dependencies {
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin"
- classpath "com.google.dagger:hilt-android-gradle-plugin:$versions.hilt"
- if (enableGoogleVariant) {
- // START Non-FOSS component
- classpath "com.google.gms:google-services:4.3.4"
- classpath "com.google.firebase:firebase-crashlytics-gradle:2.4.1"
- // END Non-FOSS component
- }
- }
-}
-
-plugins {
- id "com.android.application"
- id "org.jetbrains.kotlin.android"
- id "org.jetbrains.kotlin.kapt"
- id "com.google.protobuf" version "0.8.14"
- id "org.jetbrains.kotlin.plugin.serialization" version "1.5.10"
- id "com.google.devtools.ksp" version "1.5.10-1.0.0-beta01"
-}
-
-if (enableGoogleVariant) {
- // START Non-FOSS component
- apply plugin: "com.google.gms.google-services"
- apply plugin: "com.google.firebase.crashlytics"
- // END Non-FOSS component
-}
-
-apply plugin: "dagger.hilt.android.plugin"
-
-android {
- compileSdkVersion global.compileSdkVersion
- buildToolsVersion global.buildToolsVersion
-
- defaultConfig {
- applicationId "com.twidere.twiderex"
- minSdkVersion global.minSdkVersion
- targetSdkVersion global.targetSdkVersion
- versionCode global.versionCode
- versionName global.versionName
-
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
-
- javaCompileOptions {
- annotationProcessorOptions {
- arguments += ["room.schemaLocation": "$projectDir/schemas".toString()]
- }
- }
- def apiKeyProperties = rootProject.file("apiKey.properties")
- def hasApiKeyProps = apiKeyProperties.exists()
- if (hasApiKeyProps) {
- Properties apiKeyProp = new Properties()
- apiKeyProp.load(apiKeyProperties.newInputStream())
- buildConfigField "String", "CONSUMERKEY", apiKeyProp.get("ConsumerKey")
- buildConfigField "String", "CONSUMERSECRET", apiKeyProp.get("ConsumerSecret")
- }
-
- }
-
- lintOptions{
- disable "MissingTranslation"
- }
-
- flavorDimensions "channel"
- productFlavors {
- if (enableGoogleVariant) {
- // START Non-FOSS component
- google {
- dimension "channel"
- }
- // END Non-FOSS component
- }
- fdroid {
- dimension "channel"
- }
- }
-
- def file = rootProject.file("signing.properties")
- def hasSigningProps = file.exists()
-
- signingConfigs {
- if (hasSigningProps) {
- twidere {
- Properties signingProp = new Properties()
- signingProp.load(file.newInputStream())
- storeFile = rootProject.file(signingProp.get("storeFile"))
- storePassword = (String) signingProp.get("storePassword")
- keyAlias = (String) signingProp.get("keyAlias")
- keyPassword = (String) signingProp.get("keyPassword")
- }
- }
- }
-
- buildTypes {
- debug {
- if (hasSigningProps) {
- signingConfig signingConfigs.twidere
- }
- }
- release {
- if (hasSigningProps) {
- signingConfig signingConfigs.twidere
- }
- minifyEnabled false
- proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
- }
- }
- sourceSets.each {
- it.res.srcDirs += project.files("src/${it.name}/res-localized")
- it.java.srcDirs += "src/${it.name}/kotlin"
- }
- sourceSets {
- androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
- }
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_11
- targetCompatibility JavaVersion.VERSION_11
- }
- kotlinOptions {
- allWarningsAsErrors = true
- freeCompilerArgs = [
- "-Xopt-in=kotlin.RequiresOptIn",
- ]
- jvmTarget = "11"
- }
- buildFeatures {
- compose true
- }
- composeOptions {
- kotlinCompilerExtensionVersion libs.versions.compose.get()
- }
-
- packagingOptions {
- exclude "META-INF/AL2.0"
- exclude "META-INF/LGPL2.1"
- exclude "DebugProbesKt.bin"
- }
-}
-
-protobuf {
- protoc {
- artifact = "com.google.protobuf:protoc:${libs.versions.protobuf.get()}"
- }
- generateProtoTasks {
- all().each { task ->
- task.builtins {
- java {
- option "lite"
- }
- }
- }
- }
-}
-
-dependencies {
- implementation libs.kotlinx.serialization.json
- implementation libs.vectordrawable
-
- implementation projects.services
- ksp projects.assistedProcessor
-
- implementation libs.bundles.compose
- androidTestImplementation libs.compose.ui.test
-
- implementation libs.bundles.paging
- implementation libs.bundles.activity
- implementation libs.bundles.datastore
-
- implementation libs.bundles.hilt.base
- kapt libs.bundles.hilt.compiler
-
- implementation libs.bundles.room
- kapt libs.room.compiler
-
- implementation libs.bundles.lifecycle
-
- implementation libs.work.runtime.ktx
-
- implementation libs.startup
-
- implementation libs.coil
- implementation libs.bundles.accompanist
-
- implementation libs.zoomable
- implementation libs.nestedScrollView
- implementation libs.swiper
- implementation libs.placeholder
-
- implementation libs.twittertext
- implementation libs.jsoup
-
- debugImplementation libs.leakcanary
-
- implementation libs.protobuf.javalite
-
- implementation libs.exoplayer
-
- implementation libs.constraintLayout
-
- implementation libs.exifinterface
-
- implementation libs.browser
-
- if (enableGoogleVariant) {
- // START Non-FOSS component
- googleImplementation platform("com.google.firebase:firebase-bom:26.1.0")
- googleImplementation "com.google.firebase:firebase-analytics-ktx"
- googleImplementation "com.google.firebase:firebase-crashlytics-ktx"
- // END Non-FOSS component
- }
-
- testImplementation "junit:junit:4.13.2"
- androidTestImplementation libs.room.testing
- testImplementation libs.bundles.mockito.test
- testImplementation libs.android.test.core
- androidTestImplementation libs.android.test.core
- testImplementation libs.kotlinx.coroutines.test
- androidTestImplementation libs.bundles.androidx.test
-}
-
-apply from: "translate.gradle"
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 000000000..ca31a7f4a
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,276 @@
+import com.google.protobuf.gradle.builtins
+import com.google.protobuf.gradle.generateProtoTasks
+import com.google.protobuf.gradle.protobuf
+import com.google.protobuf.gradle.protoc
+import org.gradle.kotlin.dsl.support.unzipTo
+import org.jetbrains.kotlin.gradle.internal.ensureParentDirsCreated
+import org.json.JSONObject
+import java.util.Properties
+
+buildscript {
+ repositories {
+ google()
+ }
+
+ dependencies {
+ classpath("com.google.dagger:hilt-android-gradle-plugin:${Versions.hilt}")
+
+ if (enableGoogleVariant) {
+ // START Non-FOSS component
+ classpath("com.google.gms:google-services:4.3.5")
+ classpath("com.google.firebase:firebase-crashlytics-gradle:2.5.2")
+ // END Non-FOSS component
+ }
+ }
+}
+
+plugins {
+ id("com.android.application")
+ kotlin("android")
+ kotlin("kapt")
+ id("com.google.protobuf").version("0.8.17")
+ kotlin("plugin.serialization").version(Versions.Kotlin.lang)
+ id("com.google.devtools.ksp").version(Versions.ksp)
+}
+
+if (enableGoogleVariant) {
+ // START Non-FOSS component
+ apply(plugin = "com.google.gms.google-services")
+ apply(plugin = "com.google.firebase.crashlytics")
+ // END Non-FOSS component
+}
+apply(plugin = "dagger.hilt.android.plugin")
+
+android {
+ compileSdk = AndroidSdk.compile
+ buildToolsVersion = AndroidSdk.buildTools
+
+ defaultConfig {
+ applicationId = Package.id
+ minSdk = AndroidSdk.min
+ targetSdk = AndroidSdk.target
+ versionCode = Package.versionCode
+ versionName = Package.versionName
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+
+ javaCompileOptions {
+ annotationProcessorOptions {
+ argument("room.schemaLocation", "$projectDir/schemas")
+ }
+ }
+ val apiKeyProperties = rootProject.file("apiKey.properties")
+ val hasApiKeyProps = apiKeyProperties.exists()
+ if (hasApiKeyProps) {
+ val apiKeyProp = Properties()
+ apiKeyProp.load(apiKeyProperties.inputStream())
+ buildConfigField("String", "CONSUMERKEY", apiKeyProp.getProperty("ConsumerKey"))
+ buildConfigField("String", "CONSUMERSECRET", apiKeyProp.getProperty("ConsumerSecret"))
+ }
+ }
+
+ lint {
+ disable("MissingTranslation")
+ }
+
+ flavorDimensions.add("channel")
+ productFlavors {
+ if (enableGoogleVariant) {
+ // START Non-FOSS component
+ create("google") {
+ dimension = "channel"
+ }
+ // END Non-FOSS component
+ }
+ create("fdroid") {
+ dimension = "channel"
+ }
+ }
+
+ val file = rootProject.file("signing.properties")
+ val hasSigningProps = file.exists()
+
+ signingConfigs {
+ if (hasSigningProps) {
+ create("twidere") {
+ val signingProp = Properties()
+ signingProp.load(file.inputStream())
+ storeFile = rootProject.file(signingProp.getProperty("storeFile"))
+ storePassword = signingProp.getProperty("storePassword")
+ keyAlias = signingProp.getProperty("keyAlias")
+ keyPassword = signingProp.getProperty("keyPassword")
+ }
+ }
+ }
+
+ buildTypes {
+ debug {
+ if (hasSigningProps) {
+ signingConfig = signingConfigs.getByName("twidere")
+ }
+ }
+ release {
+ if (hasSigningProps) {
+ signingConfig = signingConfigs.getByName("twidere")
+ }
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ sourceSets.forEach {
+ it.res {
+ srcDirs(project.files("src/${it.name}/res-localized"))
+ }
+ it.java {
+ srcDirs("src/${it.name}/kotlin")
+ }
+ }
+ sourceSets {
+ findByName("androidTest")?.let {
+ it.assets {
+ srcDirs(files("$projectDir/schemas"))
+ }
+ }
+ }
+ compileOptions {
+ sourceCompatibility = Versions.Java.java
+ targetCompatibility = Versions.Java.java
+ }
+ buildFeatures {
+ compose = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = Versions.compose
+ }
+
+ packagingOptions {
+ resources {
+ excludes.addAll(
+ listOf(
+ "META-INF/AL2.0",
+ "META-INF/LGPL2.1",
+ "DebugProbesKt.bin",
+ )
+ )
+ }
+ }
+}
+
+protobuf {
+ protoc {
+ artifact = "com.google.protobuf:protoc:${Versions.protobuf}"
+ }
+ generateProtoTasks {
+ all().forEach {
+ it.builtins {
+ create("java") {
+ option("lite")
+ }
+ }
+ }
+ }
+}
+
+// TODO: workaround for https://github.com/google/ksp/issues/518
+evaluationDependsOn(":assistedProcessor")
+evaluationDependsOn(":routeProcessor")
+
+dependencies {
+ android()
+ kotlinSerialization()
+ kotlinCoroutines()
+ implementation(projects.services)
+ ksp(projects.assistedProcessor)
+ implementation(projects.routeProcessor)
+ ksp(projects.routeProcessor)
+ compose()
+ paging()
+ datastore()
+ hilt()
+ accompanist()
+ widget()
+ misc()
+
+ if (enableGoogleVariant) {
+ // START Non-FOSS component
+ val googleImplementation by configurations
+ googleImplementation(platform("com.google.firebase:firebase-bom:26.1.0"))
+ googleImplementation("com.google.firebase:firebase-analytics-ktx")
+ googleImplementation("com.google.firebase:firebase-crashlytics-ktx")
+ googleImplementation("com.google.android.play:core-ktx:1.8.1")
+ // END Non-FOSS component
+ }
+
+ junit4()
+ mockito()
+ androidTest()
+}
+
+tasks.register("generateTranslation") {
+ val localizationFolder = File(rootDir, "localization")
+ val appJson = File(localizationFolder, "app.json")
+ val target = project.file("src/main/res/values/strings.xml")
+ generateLocalization(appJson, target)
+}
+
+tasks.register("generateTranslationFromZip") {
+ val zip = File(rootProject.buildDir, "Twidere X (translations).zip")
+ val unzipTarget = rootProject.buildDir
+ unzipTo(unzipTarget, zip)
+ File(unzipTarget, "translation").listFiles()?.forEach { file ->
+ val source = File(file, "app.json")
+ val target = project.file(
+ "src/main/res-localized" + "/values-" + file.name.split('_')
+ .first() + "-r" + file.name.split('_').last() + "/strings.xml"
+ )
+ generateLocalization(source, target)
+ }
+}
+
+fun generateLocalization(appJson: File, target: File) {
+ val json = appJson.readText(Charsets.UTF_8)
+ val obj = JSONObject(json)
+ val result = flattenJson(obj).filter {
+ it.value.isNotEmpty() && it.value.isNotBlank()
+ }
+ if (result.isNotEmpty()) {
+ target.ensureParentDirsCreated()
+ target.createNewFile()
+ val xml =
+ """""" + System.lineSeparator() +
+ result.map {
+ " ${
+ it.value.replace("'", "\\'").replace(System.lineSeparator(), "\\n")
+ }"
+ }.joinToString(System.lineSeparator()) + System.lineSeparator() +
+ ""
+ target.writeText(xml)
+ }
+}
+
+fun flattenJson(obj: JSONObject): Map {
+ return obj.toMap().toList().flatMap { it ->
+ val (key, value) = it
+ when (value) {
+ is JSONObject -> {
+ flattenJson(value).map {
+ "${key}_${it.key}" to it.value
+ }.toList()
+ }
+ is Map<*, *> -> {
+ flattenJson(JSONObject(value)).map {
+ "${key}_${it.key}" to it.value
+ }.toList()
+ }
+ is String -> {
+ listOf(key to value)
+ }
+ else -> {
+ listOf(key to value.toString())
+ }
+ }
+ }.toMap()
+}
diff --git a/app/schemas/com.twidere.twiderex.db.CacheDatabase/19.json b/app/schemas/com.twidere.twiderex.db.CacheDatabase/19.json
new file mode 100644
index 000000000..e043d1e18
--- /dev/null
+++ b/app/schemas/com.twidere.twiderex.db.CacheDatabase/19.json
@@ -0,0 +1,1095 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 19,
+ "identityHash": "95078733d7caff478bf283cc54997675",
+ "entities": [
+ {
+ "tableName": "status",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` TEXT NOT NULL, `statusId` TEXT NOT NULL, `statusKey` TEXT NOT NULL, `htmlText` TEXT NOT NULL, `rawText` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `retweetCount` INTEGER NOT NULL, `likeCount` INTEGER NOT NULL, `replyCount` INTEGER NOT NULL, `placeString` TEXT, `source` TEXT NOT NULL, `hasMedia` INTEGER NOT NULL, `userKey` TEXT NOT NULL, `lang` TEXT, `is_possibly_sensitive` INTEGER NOT NULL, `platformType` TEXT NOT NULL, `mastodonExtra` TEXT, `twitterExtra` TEXT, `previewCard` TEXT, `inReplyToUserId` TEXT, `inReplyToStatusId` TEXT, PRIMARY KEY(`_id`))",
+ "fields": [
+ {
+ "fieldPath": "_id",
+ "columnName": "_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "statusId",
+ "columnName": "statusId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "statusKey",
+ "columnName": "statusKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "htmlText",
+ "columnName": "htmlText",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "rawText",
+ "columnName": "rawText",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "retweetCount",
+ "columnName": "retweetCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "likeCount",
+ "columnName": "likeCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replyCount",
+ "columnName": "replyCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "placeString",
+ "columnName": "placeString",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "source",
+ "columnName": "source",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasMedia",
+ "columnName": "hasMedia",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userKey",
+ "columnName": "userKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lang",
+ "columnName": "lang",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "is_possibly_sensitive",
+ "columnName": "is_possibly_sensitive",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "platformType",
+ "columnName": "platformType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mastodonExtra",
+ "columnName": "mastodonExtra",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "twitterExtra",
+ "columnName": "twitterExtra",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "previewCard",
+ "columnName": "previewCard",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "inReplyToUserId",
+ "columnName": "inReplyToUserId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "inReplyToStatusId",
+ "columnName": "inReplyToStatusId",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_status_statusKey",
+ "unique": true,
+ "columnNames": [
+ "statusKey"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_status_statusKey` ON `${TABLE_NAME}` (`statusKey`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "media",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` TEXT NOT NULL, `belongToKey` TEXT NOT NULL, `url` TEXT, `mediaUrl` TEXT, `previewUrl` TEXT, `type` TEXT NOT NULL, `width` INTEGER NOT NULL, `height` INTEGER NOT NULL, `pageUrl` TEXT, `altText` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
+ "fields": [
+ {
+ "fieldPath": "_id",
+ "columnName": "_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "belongToKey",
+ "columnName": "belongToKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "mediaUrl",
+ "columnName": "mediaUrl",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "previewUrl",
+ "columnName": "previewUrl",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "width",
+ "columnName": "width",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "height",
+ "columnName": "height",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pageUrl",
+ "columnName": "pageUrl",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "altText",
+ "columnName": "altText",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_media_belongToKey_order",
+ "unique": true,
+ "columnNames": [
+ "belongToKey",
+ "order"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_media_belongToKey_order` ON `${TABLE_NAME}` (`belongToKey`, `order`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "user",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` TEXT NOT NULL, `userId` TEXT NOT NULL, `name` TEXT NOT NULL, `userKey` TEXT NOT NULL, `acct` TEXT NOT NULL, `screenName` TEXT NOT NULL, `profileImage` TEXT NOT NULL, `profileBackgroundImage` TEXT, `followersCount` INTEGER NOT NULL, `friendsCount` INTEGER NOT NULL, `listedCount` INTEGER NOT NULL, `htmlDesc` TEXT NOT NULL, `rawDesc` TEXT NOT NULL, `website` TEXT, `location` TEXT, `verified` INTEGER NOT NULL, `isProtected` INTEGER NOT NULL, `platformType` TEXT NOT NULL, `statusesCount` INTEGER NOT NULL, `twitterExtra` TEXT, `mastodonExtra` TEXT, PRIMARY KEY(`_id`))",
+ "fields": [
+ {
+ "fieldPath": "_id",
+ "columnName": "_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userKey",
+ "columnName": "userKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "acct",
+ "columnName": "acct",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "screenName",
+ "columnName": "screenName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profileImage",
+ "columnName": "profileImage",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profileBackgroundImage",
+ "columnName": "profileBackgroundImage",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "followersCount",
+ "columnName": "followersCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "friendsCount",
+ "columnName": "friendsCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "listedCount",
+ "columnName": "listedCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "htmlDesc",
+ "columnName": "htmlDesc",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "rawDesc",
+ "columnName": "rawDesc",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "website",
+ "columnName": "website",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "location",
+ "columnName": "location",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "verified",
+ "columnName": "verified",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isProtected",
+ "columnName": "isProtected",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "platformType",
+ "columnName": "platformType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "statusesCount",
+ "columnName": "statusesCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "twitterExtra",
+ "columnName": "twitterExtra",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "mastodonExtra",
+ "columnName": "mastodonExtra",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_user_userKey",
+ "unique": true,
+ "columnNames": [
+ "userKey"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_userKey` ON `${TABLE_NAME}` (`userKey`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "status_reactions",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` TEXT NOT NULL, `statusKey` TEXT NOT NULL, `accountKey` TEXT NOT NULL, `liked` INTEGER NOT NULL, `retweeted` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
+ "fields": [
+ {
+ "fieldPath": "_id",
+ "columnName": "_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "statusKey",
+ "columnName": "statusKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountKey",
+ "columnName": "accountKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "liked",
+ "columnName": "liked",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "retweeted",
+ "columnName": "retweeted",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_status_reactions_statusKey_accountKey",
+ "unique": true,
+ "columnNames": [
+ "statusKey",
+ "accountKey"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_status_reactions_statusKey_accountKey` ON `${TABLE_NAME}` (`statusKey`, `accountKey`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "paging_timeline",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` TEXT NOT NULL, `accountKey` TEXT NOT NULL, `pagingKey` TEXT NOT NULL, `statusKey` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `sortId` INTEGER NOT NULL, `isGap` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
+ "fields": [
+ {
+ "fieldPath": "_id",
+ "columnName": "_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountKey",
+ "columnName": "accountKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pagingKey",
+ "columnName": "pagingKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "statusKey",
+ "columnName": "statusKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sortId",
+ "columnName": "sortId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isGap",
+ "columnName": "isGap",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_paging_timeline_accountKey_statusKey_pagingKey",
+ "unique": true,
+ "columnNames": [
+ "accountKey",
+ "statusKey",
+ "pagingKey"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_paging_timeline_accountKey_statusKey_pagingKey` ON `${TABLE_NAME}` (`accountKey`, `statusKey`, `pagingKey`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "url_entity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` TEXT NOT NULL, `statusKey` TEXT NOT NULL, `url` TEXT NOT NULL, `expandedUrl` TEXT NOT NULL, `displayUrl` TEXT NOT NULL, `title` TEXT, `description` TEXT, `image` TEXT, PRIMARY KEY(`_id`))",
+ "fields": [
+ {
+ "fieldPath": "_id",
+ "columnName": "_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "statusKey",
+ "columnName": "statusKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "expandedUrl",
+ "columnName": "expandedUrl",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "displayUrl",
+ "columnName": "displayUrl",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "image",
+ "columnName": "image",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_url_entity_statusKey_url",
+ "unique": true,
+ "columnNames": [
+ "statusKey",
+ "url"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_url_entity_statusKey_url` ON `${TABLE_NAME}` (`statusKey`, `url`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "status_reference",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` TEXT NOT NULL, `referenceType` TEXT NOT NULL, `statusKey` TEXT NOT NULL, `referenceStatusKey` TEXT NOT NULL, PRIMARY KEY(`_id`))",
+ "fields": [
+ {
+ "fieldPath": "_id",
+ "columnName": "_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "referenceType",
+ "columnName": "referenceType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "statusKey",
+ "columnName": "statusKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "referenceStatusKey",
+ "columnName": "referenceStatusKey",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_status_reference_referenceType_statusKey_referenceStatusKey",
+ "unique": true,
+ "columnNames": [
+ "referenceType",
+ "statusKey",
+ "referenceStatusKey"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_status_reference_referenceType_statusKey_referenceStatusKey` ON `${TABLE_NAME}` (`referenceType`, `statusKey`, `referenceStatusKey`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "lists",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` TEXT NOT NULL, `listId` TEXT NOT NULL, `ownerId` TEXT NOT NULL, `accountKey` TEXT NOT NULL, `listKey` TEXT NOT NULL, `title` TEXT NOT NULL, `description` TEXT NOT NULL, `mode` TEXT NOT NULL, `replyPolicy` TEXT NOT NULL, `isFollowed` INTEGER NOT NULL, `allowToSubscribe` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
+ "fields": [
+ {
+ "fieldPath": "_id",
+ "columnName": "_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "listId",
+ "columnName": "listId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "ownerId",
+ "columnName": "ownerId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountKey",
+ "columnName": "accountKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "listKey",
+ "columnName": "listKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mode",
+ "columnName": "mode",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replyPolicy",
+ "columnName": "replyPolicy",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isFollowed",
+ "columnName": "isFollowed",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "allowToSubscribe",
+ "columnName": "allowToSubscribe",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_lists_accountKey_listKey",
+ "unique": true,
+ "columnNames": [
+ "accountKey",
+ "listKey"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_lists_accountKey_listKey` ON `${TABLE_NAME}` (`accountKey`, `listKey`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "notification_cursor",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` TEXT NOT NULL, `accountKey` TEXT NOT NULL, `type` TEXT NOT NULL, `value` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
+ "fields": [
+ {
+ "fieldPath": "_id",
+ "columnName": "_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountKey",
+ "columnName": "accountKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_notification_cursor_accountKey_type",
+ "unique": true,
+ "columnNames": [
+ "accountKey",
+ "type"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_notification_cursor_accountKey_type` ON `${TABLE_NAME}` (`accountKey`, `type`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "trends",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` TEXT NOT NULL, `trendKey` TEXT NOT NULL, `accountKey` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `query` TEXT NOT NULL, `volume` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
+ "fields": [
+ {
+ "fieldPath": "_id",
+ "columnName": "_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "trendKey",
+ "columnName": "trendKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountKey",
+ "columnName": "accountKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "query",
+ "columnName": "query",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "volume",
+ "columnName": "volume",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_trends_trendKey_url",
+ "unique": true,
+ "columnNames": [
+ "trendKey",
+ "url"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_trends_trendKey_url` ON `${TABLE_NAME}` (`trendKey`, `url`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "trend_histories",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` TEXT NOT NULL, `trendKey` TEXT NOT NULL, `day` INTEGER NOT NULL, `uses` INTEGER NOT NULL, `accounts` INTEGER NOT NULL, `accountKey` TEXT NOT NULL, PRIMARY KEY(`_id`))",
+ "fields": [
+ {
+ "fieldPath": "_id",
+ "columnName": "_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "trendKey",
+ "columnName": "trendKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "day",
+ "columnName": "day",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "uses",
+ "columnName": "uses",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accounts",
+ "columnName": "accounts",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountKey",
+ "columnName": "accountKey",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_trend_histories_trendKey_day",
+ "unique": true,
+ "columnNames": [
+ "trendKey",
+ "day"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_trend_histories_trendKey_day` ON `${TABLE_NAME}` (`trendKey`, `day`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "dm_conversation",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` TEXT NOT NULL, `accountKey` TEXT NOT NULL, `conversationId` TEXT NOT NULL, `conversationKey` TEXT NOT NULL, `conversationAvatar` TEXT NOT NULL, `conversationName` TEXT NOT NULL, `conversationSubName` TEXT NOT NULL, `conversationType` TEXT NOT NULL, `recipientKey` TEXT NOT NULL, PRIMARY KEY(`_id`))",
+ "fields": [
+ {
+ "fieldPath": "_id",
+ "columnName": "_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountKey",
+ "columnName": "accountKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "conversationId",
+ "columnName": "conversationId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "conversationKey",
+ "columnName": "conversationKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "conversationAvatar",
+ "columnName": "conversationAvatar",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "conversationName",
+ "columnName": "conversationName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "conversationSubName",
+ "columnName": "conversationSubName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "conversationType",
+ "columnName": "conversationType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "recipientKey",
+ "columnName": "recipientKey",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_dm_conversation_accountKey_conversationKey",
+ "unique": true,
+ "columnNames": [
+ "accountKey",
+ "conversationKey"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_dm_conversation_accountKey_conversationKey` ON `${TABLE_NAME}` (`accountKey`, `conversationKey`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "dm_event",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` TEXT NOT NULL, `accountKey` TEXT NOT NULL, `sortId` INTEGER NOT NULL, `conversationKey` TEXT NOT NULL, `messageId` TEXT NOT NULL, `messageKey` TEXT NOT NULL, `htmlText` TEXT NOT NULL, `originText` TEXT NOT NULL, `createdTimestamp` INTEGER NOT NULL, `messageType` TEXT NOT NULL, `senderAccountKey` TEXT NOT NULL, `recipientAccountKey` TEXT NOT NULL, `sendStatus` TEXT NOT NULL, PRIMARY KEY(`_id`))",
+ "fields": [
+ {
+ "fieldPath": "_id",
+ "columnName": "_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountKey",
+ "columnName": "accountKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sortId",
+ "columnName": "sortId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "conversationKey",
+ "columnName": "conversationKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "messageId",
+ "columnName": "messageId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "messageKey",
+ "columnName": "messageKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "htmlText",
+ "columnName": "htmlText",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "originText",
+ "columnName": "originText",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdTimestamp",
+ "columnName": "createdTimestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "messageType",
+ "columnName": "messageType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "senderAccountKey",
+ "columnName": "senderAccountKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "recipientAccountKey",
+ "columnName": "recipientAccountKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sendStatus",
+ "columnName": "sendStatus",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_dm_event_accountKey_conversationKey_messageKey",
+ "unique": true,
+ "columnNames": [
+ "accountKey",
+ "conversationKey",
+ "messageKey"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_dm_event_accountKey_conversationKey_messageKey` ON `${TABLE_NAME}` (`accountKey`, `conversationKey`, `messageKey`)"
+ }
+ ],
+ "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, '95078733d7caff478bf283cc54997675')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.twidere.twiderex.db.CacheDatabase/20.json b/app/schemas/com.twidere.twiderex.db.CacheDatabase/20.json
new file mode 100644
index 000000000..22a622a8e
--- /dev/null
+++ b/app/schemas/com.twidere.twiderex.db.CacheDatabase/20.json
@@ -0,0 +1,1083 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 20,
+ "identityHash": "a371ddef3fc92cdd0d0914cd1df452a2",
+ "entities": [
+ {
+ "tableName": "status",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` TEXT NOT NULL, `statusId` TEXT NOT NULL, `statusKey` TEXT NOT NULL, `htmlText` TEXT NOT NULL, `rawText` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `retweetCount` INTEGER NOT NULL, `likeCount` INTEGER NOT NULL, `replyCount` INTEGER NOT NULL, `placeString` TEXT, `source` TEXT NOT NULL, `hasMedia` INTEGER NOT NULL, `userKey` TEXT NOT NULL, `lang` TEXT, `is_possibly_sensitive` INTEGER NOT NULL, `platformType` TEXT NOT NULL, `previewCard` TEXT, `inReplyToUserId` TEXT, `inReplyToStatusId` TEXT, `extra` TEXT NOT NULL, PRIMARY KEY(`_id`))",
+ "fields": [
+ {
+ "fieldPath": "_id",
+ "columnName": "_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "statusId",
+ "columnName": "statusId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "statusKey",
+ "columnName": "statusKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "htmlText",
+ "columnName": "htmlText",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "rawText",
+ "columnName": "rawText",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "retweetCount",
+ "columnName": "retweetCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "likeCount",
+ "columnName": "likeCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replyCount",
+ "columnName": "replyCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "placeString",
+ "columnName": "placeString",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "source",
+ "columnName": "source",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasMedia",
+ "columnName": "hasMedia",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userKey",
+ "columnName": "userKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lang",
+ "columnName": "lang",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "is_possibly_sensitive",
+ "columnName": "is_possibly_sensitive",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "platformType",
+ "columnName": "platformType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "previewCard",
+ "columnName": "previewCard",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "inReplyToUserId",
+ "columnName": "inReplyToUserId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "inReplyToStatusId",
+ "columnName": "inReplyToStatusId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "extra",
+ "columnName": "extra",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_status_statusKey",
+ "unique": true,
+ "columnNames": [
+ "statusKey"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_status_statusKey` ON `${TABLE_NAME}` (`statusKey`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "media",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` TEXT NOT NULL, `belongToKey` TEXT NOT NULL, `url` TEXT, `mediaUrl` TEXT, `previewUrl` TEXT, `type` TEXT NOT NULL, `width` INTEGER NOT NULL, `height` INTEGER NOT NULL, `pageUrl` TEXT, `altText` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
+ "fields": [
+ {
+ "fieldPath": "_id",
+ "columnName": "_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "belongToKey",
+ "columnName": "belongToKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "mediaUrl",
+ "columnName": "mediaUrl",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "previewUrl",
+ "columnName": "previewUrl",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "width",
+ "columnName": "width",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "height",
+ "columnName": "height",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pageUrl",
+ "columnName": "pageUrl",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "altText",
+ "columnName": "altText",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_media_belongToKey_order",
+ "unique": true,
+ "columnNames": [
+ "belongToKey",
+ "order"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_media_belongToKey_order` ON `${TABLE_NAME}` (`belongToKey`, `order`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "user",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` TEXT NOT NULL, `userId` TEXT NOT NULL, `name` TEXT NOT NULL, `userKey` TEXT NOT NULL, `acct` TEXT NOT NULL, `screenName` TEXT NOT NULL, `profileImage` TEXT NOT NULL, `profileBackgroundImage` TEXT, `followersCount` INTEGER NOT NULL, `friendsCount` INTEGER NOT NULL, `listedCount` INTEGER NOT NULL, `htmlDesc` TEXT NOT NULL, `rawDesc` TEXT NOT NULL, `website` TEXT, `location` TEXT, `verified` INTEGER NOT NULL, `isProtected` INTEGER NOT NULL, `platformType` TEXT NOT NULL, `statusesCount` INTEGER NOT NULL, `extra` TEXT NOT NULL, PRIMARY KEY(`_id`))",
+ "fields": [
+ {
+ "fieldPath": "_id",
+ "columnName": "_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userKey",
+ "columnName": "userKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "acct",
+ "columnName": "acct",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "screenName",
+ "columnName": "screenName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profileImage",
+ "columnName": "profileImage",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profileBackgroundImage",
+ "columnName": "profileBackgroundImage",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "followersCount",
+ "columnName": "followersCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "friendsCount",
+ "columnName": "friendsCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "listedCount",
+ "columnName": "listedCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "htmlDesc",
+ "columnName": "htmlDesc",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "rawDesc",
+ "columnName": "rawDesc",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "website",
+ "columnName": "website",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "location",
+ "columnName": "location",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "verified",
+ "columnName": "verified",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isProtected",
+ "columnName": "isProtected",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "platformType",
+ "columnName": "platformType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "statusesCount",
+ "columnName": "statusesCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "extra",
+ "columnName": "extra",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_user_userKey",
+ "unique": true,
+ "columnNames": [
+ "userKey"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_userKey` ON `${TABLE_NAME}` (`userKey`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "status_reactions",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` TEXT NOT NULL, `statusKey` TEXT NOT NULL, `accountKey` TEXT NOT NULL, `liked` INTEGER NOT NULL, `retweeted` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
+ "fields": [
+ {
+ "fieldPath": "_id",
+ "columnName": "_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "statusKey",
+ "columnName": "statusKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountKey",
+ "columnName": "accountKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "liked",
+ "columnName": "liked",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "retweeted",
+ "columnName": "retweeted",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_status_reactions_statusKey_accountKey",
+ "unique": true,
+ "columnNames": [
+ "statusKey",
+ "accountKey"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_status_reactions_statusKey_accountKey` ON `${TABLE_NAME}` (`statusKey`, `accountKey`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "paging_timeline",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` TEXT NOT NULL, `accountKey` TEXT NOT NULL, `pagingKey` TEXT NOT NULL, `statusKey` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `sortId` INTEGER NOT NULL, `isGap` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
+ "fields": [
+ {
+ "fieldPath": "_id",
+ "columnName": "_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountKey",
+ "columnName": "accountKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pagingKey",
+ "columnName": "pagingKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "statusKey",
+ "columnName": "statusKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sortId",
+ "columnName": "sortId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isGap",
+ "columnName": "isGap",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_paging_timeline_accountKey_statusKey_pagingKey",
+ "unique": true,
+ "columnNames": [
+ "accountKey",
+ "statusKey",
+ "pagingKey"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_paging_timeline_accountKey_statusKey_pagingKey` ON `${TABLE_NAME}` (`accountKey`, `statusKey`, `pagingKey`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "url_entity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` TEXT NOT NULL, `statusKey` TEXT NOT NULL, `url` TEXT NOT NULL, `expandedUrl` TEXT NOT NULL, `displayUrl` TEXT NOT NULL, `title` TEXT, `description` TEXT, `image` TEXT, PRIMARY KEY(`_id`))",
+ "fields": [
+ {
+ "fieldPath": "_id",
+ "columnName": "_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "statusKey",
+ "columnName": "statusKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "expandedUrl",
+ "columnName": "expandedUrl",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "displayUrl",
+ "columnName": "displayUrl",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "image",
+ "columnName": "image",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_url_entity_statusKey_url",
+ "unique": true,
+ "columnNames": [
+ "statusKey",
+ "url"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_url_entity_statusKey_url` ON `${TABLE_NAME}` (`statusKey`, `url`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "status_reference",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` TEXT NOT NULL, `referenceType` TEXT NOT NULL, `statusKey` TEXT NOT NULL, `referenceStatusKey` TEXT NOT NULL, PRIMARY KEY(`_id`))",
+ "fields": [
+ {
+ "fieldPath": "_id",
+ "columnName": "_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "referenceType",
+ "columnName": "referenceType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "statusKey",
+ "columnName": "statusKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "referenceStatusKey",
+ "columnName": "referenceStatusKey",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_status_reference_referenceType_statusKey_referenceStatusKey",
+ "unique": true,
+ "columnNames": [
+ "referenceType",
+ "statusKey",
+ "referenceStatusKey"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_status_reference_referenceType_statusKey_referenceStatusKey` ON `${TABLE_NAME}` (`referenceType`, `statusKey`, `referenceStatusKey`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "lists",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` TEXT NOT NULL, `listId` TEXT NOT NULL, `ownerId` TEXT NOT NULL, `accountKey` TEXT NOT NULL, `listKey` TEXT NOT NULL, `title` TEXT NOT NULL, `description` TEXT NOT NULL, `mode` TEXT NOT NULL, `replyPolicy` TEXT NOT NULL, `isFollowed` INTEGER NOT NULL, `allowToSubscribe` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
+ "fields": [
+ {
+ "fieldPath": "_id",
+ "columnName": "_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "listId",
+ "columnName": "listId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "ownerId",
+ "columnName": "ownerId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountKey",
+ "columnName": "accountKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "listKey",
+ "columnName": "listKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mode",
+ "columnName": "mode",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "replyPolicy",
+ "columnName": "replyPolicy",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isFollowed",
+ "columnName": "isFollowed",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "allowToSubscribe",
+ "columnName": "allowToSubscribe",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_lists_accountKey_listKey",
+ "unique": true,
+ "columnNames": [
+ "accountKey",
+ "listKey"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_lists_accountKey_listKey` ON `${TABLE_NAME}` (`accountKey`, `listKey`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "notification_cursor",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` TEXT NOT NULL, `accountKey` TEXT NOT NULL, `type` TEXT NOT NULL, `value` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
+ "fields": [
+ {
+ "fieldPath": "_id",
+ "columnName": "_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountKey",
+ "columnName": "accountKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_notification_cursor_accountKey_type",
+ "unique": true,
+ "columnNames": [
+ "accountKey",
+ "type"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_notification_cursor_accountKey_type` ON `${TABLE_NAME}` (`accountKey`, `type`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "trends",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` TEXT NOT NULL, `trendKey` TEXT NOT NULL, `accountKey` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `query` TEXT NOT NULL, `volume` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
+ "fields": [
+ {
+ "fieldPath": "_id",
+ "columnName": "_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "trendKey",
+ "columnName": "trendKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountKey",
+ "columnName": "accountKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "query",
+ "columnName": "query",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "volume",
+ "columnName": "volume",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_trends_trendKey_url",
+ "unique": true,
+ "columnNames": [
+ "trendKey",
+ "url"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_trends_trendKey_url` ON `${TABLE_NAME}` (`trendKey`, `url`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "trend_histories",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` TEXT NOT NULL, `trendKey` TEXT NOT NULL, `day` INTEGER NOT NULL, `uses` INTEGER NOT NULL, `accounts` INTEGER NOT NULL, `accountKey` TEXT NOT NULL, PRIMARY KEY(`_id`))",
+ "fields": [
+ {
+ "fieldPath": "_id",
+ "columnName": "_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "trendKey",
+ "columnName": "trendKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "day",
+ "columnName": "day",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "uses",
+ "columnName": "uses",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accounts",
+ "columnName": "accounts",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountKey",
+ "columnName": "accountKey",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_trend_histories_trendKey_day",
+ "unique": true,
+ "columnNames": [
+ "trendKey",
+ "day"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_trend_histories_trendKey_day` ON `${TABLE_NAME}` (`trendKey`, `day`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "dm_conversation",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` TEXT NOT NULL, `accountKey` TEXT NOT NULL, `conversationId` TEXT NOT NULL, `conversationKey` TEXT NOT NULL, `conversationAvatar` TEXT NOT NULL, `conversationName` TEXT NOT NULL, `conversationSubName` TEXT NOT NULL, `conversationType` TEXT NOT NULL, `recipientKey` TEXT NOT NULL, PRIMARY KEY(`_id`))",
+ "fields": [
+ {
+ "fieldPath": "_id",
+ "columnName": "_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountKey",
+ "columnName": "accountKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "conversationId",
+ "columnName": "conversationId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "conversationKey",
+ "columnName": "conversationKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "conversationAvatar",
+ "columnName": "conversationAvatar",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "conversationName",
+ "columnName": "conversationName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "conversationSubName",
+ "columnName": "conversationSubName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "conversationType",
+ "columnName": "conversationType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "recipientKey",
+ "columnName": "recipientKey",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_dm_conversation_accountKey_conversationKey",
+ "unique": true,
+ "columnNames": [
+ "accountKey",
+ "conversationKey"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_dm_conversation_accountKey_conversationKey` ON `${TABLE_NAME}` (`accountKey`, `conversationKey`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "dm_event",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` TEXT NOT NULL, `accountKey` TEXT NOT NULL, `sortId` INTEGER NOT NULL, `conversationKey` TEXT NOT NULL, `messageId` TEXT NOT NULL, `messageKey` TEXT NOT NULL, `htmlText` TEXT NOT NULL, `originText` TEXT NOT NULL, `createdTimestamp` INTEGER NOT NULL, `messageType` TEXT NOT NULL, `senderAccountKey` TEXT NOT NULL, `recipientAccountKey` TEXT NOT NULL, `sendStatus` TEXT NOT NULL, PRIMARY KEY(`_id`))",
+ "fields": [
+ {
+ "fieldPath": "_id",
+ "columnName": "_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountKey",
+ "columnName": "accountKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sortId",
+ "columnName": "sortId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "conversationKey",
+ "columnName": "conversationKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "messageId",
+ "columnName": "messageId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "messageKey",
+ "columnName": "messageKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "htmlText",
+ "columnName": "htmlText",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "originText",
+ "columnName": "originText",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdTimestamp",
+ "columnName": "createdTimestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "messageType",
+ "columnName": "messageType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "senderAccountKey",
+ "columnName": "senderAccountKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "recipientAccountKey",
+ "columnName": "recipientAccountKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sendStatus",
+ "columnName": "sendStatus",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_dm_event_accountKey_conversationKey_messageKey",
+ "unique": true,
+ "columnNames": [
+ "accountKey",
+ "conversationKey",
+ "messageKey"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_dm_event_accountKey_conversationKey_messageKey` ON `${TABLE_NAME}` (`accountKey`, `conversationKey`, `messageKey`)"
+ }
+ ],
+ "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, 'a371ddef3fc92cdd0d0914cd1df452a2')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/twidere/twiderex/db/DbDMEventTest.kt b/app/src/androidTest/java/com/twidere/twiderex/db/DbDMEventTest.kt
index 9f364bff5..dab27abc7 100644
--- a/app/src/androidTest/java/com/twidere/twiderex/db/DbDMEventTest.kt
+++ b/app/src/androidTest/java/com/twidere/twiderex/db/DbDMEventTest.kt
@@ -50,6 +50,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.util.UUID
+import java.util.concurrent.Executors
@RunWith(AndroidJUnit4::class)
class DbDMEventTest {
@@ -64,7 +65,7 @@ class DbDMEventTest {
@Before
fun setUp() {
cacheDatabase = Room.inMemoryDatabaseBuilder(ApplicationProvider.getApplicationContext(), CacheDatabase::class.java)
- .build()
+ .setTransactionExecutor(Executors.newSingleThreadExecutor()).build()
runBlocking {
for (i in 0 until conversationCount) {
generateDirectMessage(
diff --git a/app/src/androidTest/java/com/twidere/twiderex/db/DbListTest.kt b/app/src/androidTest/java/com/twidere/twiderex/db/DbListTest.kt
index 691c19de6..795326b83 100644
--- a/app/src/androidTest/java/com/twidere/twiderex/db/DbListTest.kt
+++ b/app/src/androidTest/java/com/twidere/twiderex/db/DbListTest.kt
@@ -21,7 +21,6 @@
package com.twidere.twiderex.db
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
-import androidx.lifecycle.Observer
import androidx.paging.PagingSource
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
@@ -32,8 +31,8 @@ import com.twidere.services.twitter.model.TwitterList
import com.twidere.services.twitter.model.User
import com.twidere.twiderex.db.dao.ListsDao
import com.twidere.twiderex.db.mapper.toDbList
-import com.twidere.twiderex.db.model.DbList
import com.twidere.twiderex.model.MicroBlogKey
+import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert
@@ -41,6 +40,7 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import java.util.concurrent.Executors
@RunWith(AndroidJUnit4::class)
class DbListTest {
@@ -57,7 +57,7 @@ class DbListTest {
@Before
fun setUp() {
cacheDatabase = Room.inMemoryDatabaseBuilder(ApplicationProvider.getApplicationContext(), CacheDatabase::class.java)
- .build()
+ .setTransactionExecutor(Executors.newSingleThreadExecutor()).build()
listsDao = cacheDatabase.listsDao()
for (i in 0 until twitterCount) {
val ownerId = if (i % 2 == 0) twitterAccountKey.id else "789"
@@ -116,17 +116,16 @@ class DbListTest {
}
@Test
- fun findDbListWithListKeyWithLiveData_AutoUpdateAfterDbUpdate() {
+ fun findDbListWithListKeyWithFlow_AutoUpdateAfterDbUpdate() {
runBlocking {
- val source = listsDao.findWithListKeyWithLiveData(MicroBlogKey.twitter("0"), twitterAccountKey)
- val observer = Observer { }
- source.observeForever(observer)
- Assert.assertEquals("description 0", source.value?.description)
- source.value?.let {
+ val source = listsDao.findWithListKeyWithFlow(MicroBlogKey.twitter("0"), twitterAccountKey)
+ var data = source.firstOrNull()
+ Assert.assertEquals("description 0", data?.description)
+ data?.let {
listsDao.update(listOf(it.copy(description = "Update 0")))
}
- Assert.assertEquals("Update 0", source.value?.description)
- source.removeObserver(observer)
+ data = source.firstOrNull()
+ Assert.assertEquals("Update 0", data?.description)
}
}
diff --git a/app/src/androidTest/java/com/twidere/twiderex/db/DbSearchTest.kt b/app/src/androidTest/java/com/twidere/twiderex/db/DbSearchTest.kt
index 4b602f90c..37d50cf86 100644
--- a/app/src/androidTest/java/com/twidere/twiderex/db/DbSearchTest.kt
+++ b/app/src/androidTest/java/com/twidere/twiderex/db/DbSearchTest.kt
@@ -21,13 +21,13 @@
package com.twidere.twiderex.db
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
-import androidx.lifecycle.Observer
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.twidere.twiderex.db.dao.SearchDao
import com.twidere.twiderex.db.model.DbSearch
import com.twidere.twiderex.model.MicroBlogKey
+import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert
@@ -92,20 +92,17 @@ class DbSearchTest {
@Test
fun getAll_returnResultByAccountKey() = runBlocking {
val result = searchDao.getAll(twitterAccountKey)
- val observer = Observer?> { }
- result.observeForever(observer)
- Assert.assertEquals(twitterSearchCount, result.value?.size)
- result.value?.forEach {
+ val value = result.firstOrNull()
+ Assert.assertEquals(twitterSearchCount, value?.size)
+ value?.forEach {
assert(it.content.startsWith("twitter"))
} ?: assert(false)
}
@Test
fun getAllHistory_returnResultsMatchAccountKeyAndNotSaved() = runBlocking {
- val result = searchDao.getAllHistory(mastodonAccountKey)
- val observer = Observer?> { }
- result.observeForever(observer)
- result.value?.forEach {
+ val result = searchDao.getAllHistory(mastodonAccountKey).firstOrNull()
+ result?.forEach {
assert(it.content.startsWith("mastodon"))
assert(!it.saved)
} ?: assert(false)
@@ -113,10 +110,8 @@ class DbSearchTest {
@Test
fun getAllSaved_returnResultsMatchAccountKeyAndSaved() = runBlocking {
- val result = searchDao.getAllSaved(twitterAccountKey)
- val observer = Observer?> { }
- result.observeForever(observer)
- result.value?.forEach {
+ val result = searchDao.getAllSaved(twitterAccountKey).firstOrNull()
+ result?.forEach {
assert(it.content.startsWith("twitter"))
assert(it.saved)
} ?: assert(false)
@@ -141,16 +136,13 @@ class DbSearchTest {
@Test
fun clearHistory_deleteSearchNotSaved() = runBlocking {
searchDao.clear()
- val twitterResult = searchDao.getAll(twitterAccountKey)
- val mastodonResult = searchDao.getAll(mastodonAccountKey)
- val observer = Observer?> { }
- twitterResult.observeForever(observer)
- mastodonResult.observeForever(observer)
- twitterResult.value?.forEach {
+ val twitterResult = searchDao.getAll(twitterAccountKey).firstOrNull()
+ val mastodonResult = searchDao.getAll(mastodonAccountKey).firstOrNull()
+ twitterResult?.forEach {
assert(it.saved)
} ?: assert(false)
- mastodonResult.value?.forEach {
+ mastodonResult?.forEach {
assert(it.saved)
} ?: assert(false)
}
diff --git a/app/src/androidTest/java/com/twidere/twiderex/db/DbTrendTest.kt b/app/src/androidTest/java/com/twidere/twiderex/db/DbTrendTest.kt
index 0661e6335..49c706c17 100644
--- a/app/src/androidTest/java/com/twidere/twiderex/db/DbTrendTest.kt
+++ b/app/src/androidTest/java/com/twidere/twiderex/db/DbTrendTest.kt
@@ -37,6 +37,7 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import java.util.concurrent.Executors
typealias TwitterTrend = com.twidere.services.twitter.model.Trend
typealias MastodonTrend = com.twidere.services.mastodon.model.Trend
@@ -57,7 +58,7 @@ class DbTrendTest {
@Before
fun setUp() {
cacheDatabase = Room.inMemoryDatabaseBuilder(ApplicationProvider.getApplicationContext(), CacheDatabase::class.java)
- .build()
+ .setTransactionExecutor(Executors.newSingleThreadExecutor()).build()
for (i in 0 until twitterTrendCount) {
trends.add(
TwitterTrend(
diff --git a/app/src/androidTest/java/com/twidere/twiderex/repository/dm/DirectMessageRepositoryTest.kt b/app/src/androidTest/java/com/twidere/twiderex/repository/dm/DirectMessageRepositoryTest.kt
index 41944f76a..8fb3d77a7 100644
--- a/app/src/androidTest/java/com/twidere/twiderex/repository/dm/DirectMessageRepositoryTest.kt
+++ b/app/src/androidTest/java/com/twidere/twiderex/repository/dm/DirectMessageRepositoryTest.kt
@@ -30,8 +30,8 @@ import com.twidere.twiderex.db.mapper.toDbUser
import com.twidere.twiderex.mock.MockDirectMessageService
import com.twidere.twiderex.mock.MockLookUpService
import com.twidere.twiderex.model.MicroBlogKey
-import com.twidere.twiderex.model.PlatformType
-import com.twidere.twiderex.model.ui.UiUser.Companion.toUi
+import com.twidere.twiderex.model.enums.PlatformType
+import com.twidere.twiderex.model.transform.toUi
import com.twidere.twiderex.repository.DirectMessageRepository
import kotlinx.coroutines.runBlocking
import org.junit.After
diff --git a/app/src/main/kotlin/com/twidere/twiderex/extensions/LiveDataExtensions.kt b/app/src/fdroid/kotlin/com.twidere.twiderex/MissingSplitsCheckerImpl.kt
similarity index 62%
rename from app/src/main/kotlin/com/twidere/twiderex/extensions/LiveDataExtensions.kt
rename to app/src/fdroid/kotlin/com.twidere.twiderex/MissingSplitsCheckerImpl.kt
index 65807b631..e3d5cb336 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/extensions/LiveDataExtensions.kt
+++ b/app/src/fdroid/kotlin/com.twidere.twiderex/MissingSplitsCheckerImpl.kt
@@ -18,21 +18,12 @@
* You should have received a copy of the GNU General Public License
* along with Twidere X. If not, see .
*/
-package com.twidere.twiderex.extensions
+package com.twidere.twiderex
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MediatorLiveData
+import android.content.Context
-fun LiveData.combineWith(
- liveData: LiveData,
- block: (T?, K?) -> R
-): LiveData {
- val result = MediatorLiveData()
- result.addSource(this) {
- result.value = block(this.value, liveData.value)
+class MissingSplitsCheckerImpl : TwidereApp.MissingSplitsChecker {
+ override fun requiredSplits(context: Context): Boolean {
+ return false
}
- result.addSource(liveData) {
- result.value = block(this.value, liveData.value)
- }
- return result
}
diff --git a/app/src/google/kotlin/com.twidere.twiderex/MissingSplitsCheckerImpl.kt b/app/src/google/kotlin/com.twidere.twiderex/MissingSplitsCheckerImpl.kt
new file mode 100644
index 000000000..72dc225a6
--- /dev/null
+++ b/app/src/google/kotlin/com.twidere.twiderex/MissingSplitsCheckerImpl.kt
@@ -0,0 +1,29 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex
+import android.content.Context
+import com.google.android.play.core.missingsplits.MissingSplitsManagerFactory
+
+class MissingSplitsCheckerImpl : TwidereApp.MissingSplitsChecker {
+ override fun requiredSplits(context: Context): Boolean {
+ return MissingSplitsManagerFactory.create(context).disableAppIfMissingRequiredSplits()
+ }
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 768fec236..ea12dd30e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -42,6 +42,10 @@
android:name="com.twidere.twiderex.notification.NotificationInitializer"
android:value="androidx.startup"
tools:node="remove" />
+
+
- *
- * This file is part of Twidere X.
- *
- * Twidere X is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * Twidere X is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Twidere X. If not, see .
- */
-package androidx.paging.compose
-
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import android.annotation.SuppressLint
-import android.os.Parcel
-import android.os.Parcelable
-import androidx.compose.foundation.lazy.LazyItemScope
-import androidx.compose.foundation.lazy.LazyListScope
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.State
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.runtime.setValue
-import androidx.paging.CombinedLoadStates
-import androidx.paging.DifferCallback
-import androidx.paging.ItemSnapshotList
-import androidx.paging.LoadState
-import androidx.paging.LoadStates
-import androidx.paging.NullPaddedList
-import androidx.paging.PagingConfig
-import androidx.paging.PagingData
-import androidx.paging.PagingDataDiffer
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.flow.collectLatest
-
-/**
- * The class responsible for accessing the data from a [Flow] of [PagingData].
- * In order to obtain an instance of [LazyPagingItems] use the [collectAsLazyPagingItems] extension
- * method of [Flow] with [PagingData].
- * This instance can be used by the [items] and [itemsIndexed] methods inside [LazyListScope] to
- * display data received from the [Flow] of [PagingData].
- *
- * @param T the type of value used by [PagingData].
- */
-public class LazyPagingItems internal constructor(
- /**
- * the [Flow] object which contains a stream of [PagingData] elements.
- */
- private val flow: Flow>
-) {
- private val mainDispatcher = Dispatchers.Main
-
- /**
- * Contains the latest items list snapshot collected from the [flow].
- */
- private var itemSnapshotList by mutableStateOf(
- ItemSnapshotList(0, 0, emptyList())
- )
-
- /**
- * The number of items which can be accessed.
- */
- val itemCount: Int get() = itemSnapshotList.size
-
- @SuppressLint("RestrictedApi")
- private val differCallback: DifferCallback = object : DifferCallback {
- override fun onChanged(position: Int, count: Int) {
- if (count > 0) {
- updateItemSnapshotList()
- }
- }
-
- override fun onInserted(position: Int, count: Int) {
- if (count > 0) {
- updateItemSnapshotList()
- }
- }
-
- override fun onRemoved(position: Int, count: Int) {
- if (count > 0) {
- updateItemSnapshotList()
- }
- }
- }
-
- private val pagingDataDiffer = object : PagingDataDiffer(
- differCallback = differCallback,
- mainDispatcher = mainDispatcher
- ) {
- override suspend fun presentNewList(
- previousList: NullPaddedList,
- newList: NullPaddedList,
- newCombinedLoadStates: CombinedLoadStates,
- lastAccessedIndex: Int,
- onListPresentable: () -> Unit
- ): Int? {
- onListPresentable()
- updateItemSnapshotList()
- return null
- }
- }
-
- private fun updateItemSnapshotList() {
- itemSnapshotList = pagingDataDiffer.snapshot()
- }
-
- /**
- * Returns the presented item at the specified position, notifying Paging of the item access to
- * trigger any loads necessary to fulfill prefetchDistance.
- *
- * @see peek
- */
- operator fun get(index: Int): T? {
- pagingDataDiffer[index] // this registers the value load
- return itemSnapshotList[index]
- }
-
- /**
- * Returns the state containing the item specified at [index] and notifies Paging of the item
- * accessed in order to trigger any loads necessary to fulfill [PagingConfig.prefetchDistance].
- *
- * @param index the index of the item which should be returned.
- * @return the state containing the item specified at [index] or null if the item is a
- * placeholder or [index] is not within the correct bounds.
- */
- @Composable
- @Deprecated(
- "Use get() instead. It will return you the value not wrapped into a State",
- ReplaceWith("this[index]")
- )
- fun getAsState(index: Int): State {
- return rememberUpdatedState(get(index))
- }
-
- /**
- * Returns the presented item at the specified position, without notifying Paging of the item
- * access that would normally trigger page loads.
- *
- * @param index Index of the presented item to return, including placeholders.
- * @return The presented item at position [index], `null` if it is a placeholder
- */
- fun peek(index: Int): T? {
- return itemSnapshotList[index]
- }
-
- /**
- * Returns a new [ItemSnapshotList] representing the currently presented items, including any
- * placeholders if they are enabled.
- */
- fun snapshot(): ItemSnapshotList {
- return itemSnapshotList
- }
-
- /**
- * Retry any failed load requests that would result in a [LoadState.Error] update to this
- * [LazyPagingItems].
- *
- * Unlike [refresh], this does not invalidate [PagingSource], it only retries failed loads
- * within the same generation of [PagingData].
- *
- * [LoadState.Error] can be generated from two types of load requests:
- * * [PagingSource.load] returning [PagingSource.LoadResult.Error]
- * * [RemoteMediator.load] returning [RemoteMediator.MediatorResult.Error]
- */
- fun retry() {
- pagingDataDiffer.retry()
- }
-
- /**
- * Refresh the data presented by this [LazyPagingItems].
- *
- * [refresh] triggers the creation of a new [PagingData] with a new instance of [PagingSource]
- * to represent an updated snapshot of the backing dataset. If a [RemoteMediator] is set,
- * calling [refresh] will also trigger a call to [RemoteMediator.load] with [LoadType] [REFRESH]
- * to allow [RemoteMediator] to check for updates to the dataset backing [PagingSource].
- *
- * Note: This API is intended for UI-driven refresh signals, such as swipe-to-refresh.
- * Invalidation due repository-layer signals, such as DB-updates, should instead use
- * [PagingSource.invalidate].
- *
- * @see PagingSource.invalidate
- */
- fun refresh() {
- pagingDataDiffer.refresh()
- }
-
- /**
- * A [CombinedLoadStates] object which represents the current loading state.
- */
- public var loadState: CombinedLoadStates by mutableStateOf(
- CombinedLoadStates(
- refresh = InitialLoadStates.refresh,
- prepend = InitialLoadStates.prepend,
- append = InitialLoadStates.append,
- source = InitialLoadStates
- )
- )
- private set
-
- internal suspend fun collectLoadState() {
- pagingDataDiffer.loadStateFlow.collect {
- loadState = it
- }
- }
-
- internal suspend fun collectPagingData() {
- flow.collectLatest {
- pagingDataDiffer.collectFrom(it)
- }
- }
-}
-
-private val IncompleteLoadState = LoadState.NotLoading(false)
-private val InitialLoadStates = LoadStates(
- IncompleteLoadState,
- IncompleteLoadState,
- IncompleteLoadState
-)
-
-/**
- * Collects values from this [Flow] of [PagingData] and represents them inside a [LazyPagingItems]
- * instance. The [LazyPagingItems] instance can be used by the [items] and [itemsIndexed] methods
- * from [LazyListScope] in order to display the data obtained from a [Flow] of [PagingData].
- *
- * @sample androidx.paging.compose.samples.PagingBackendSample
- */
-@Composable
-public fun Flow>.collectAsLazyPagingItems(): LazyPagingItems {
- val lazyPagingItems = remember(this) { LazyPagingItems(this) }
-
- LaunchedEffect(lazyPagingItems) {
- lazyPagingItems.collectPagingData()
- }
- LaunchedEffect(lazyPagingItems) {
- lazyPagingItems.collectLoadState()
- }
-
- return lazyPagingItems
-}
-
-/**
- * Adds the [LazyPagingItems] and their content to the scope. The range from 0 (inclusive) to
- * [LazyPagingItems.itemCount] (exclusive) always represents the full range of presentable items,
- * because every event from [PagingDataDiffer] will trigger a recomposition.
- *
- * @sample androidx.paging.compose.samples.ItemsDemo
- *
- * @param items the items received from a [Flow] of [PagingData].
- * @param key a factory of stable and unique keys representing the item. Using the same key
- * for multiple items in the list is not allowed. Type of the key should be saveable
- * via Bundle on Android. If null is passed the position in the list will represent the key.
- * When you specify the key the scroll position will be maintained based on the key, which
- * means if you add/remove items before the current visible item the item with the given key
- * will be kept as the first visible one.
- * @param itemContent the content displayed by a single item. In case the item is `null`, the
- * [itemContent] method should handle the logic of displaying a placeholder instead of the main
- * content displayed by an item which is not `null`.
- */
-public fun LazyListScope.items(
- items: LazyPagingItems,
- key: ((item: T) -> Any)? = null,
- itemContent: @Composable LazyItemScope.(value: T?) -> Unit
-) {
- items(
- count = items.itemCount,
- key = if (key == null) null else { index ->
- val item = items.peek(index)
- if (item == null) {
- PagingPlaceholderKey(index)
- } else {
- key(item)
- }
- }
- ) { index ->
- itemContent(items[index])
- }
-}
-
-/**
- * Adds the [LazyPagingItems] and their content to the scope where the content of an item is
- * aware of its local index. The range from 0 (inclusive) to [LazyPagingItems.itemCount] (exclusive)
- * always represents the full range of presentable items, because every event from
- * [PagingDataDiffer] will trigger a recomposition.
- *
- * @sample androidx.paging.compose.samples.ItemsIndexedDemo
- *
- * @param items the items received from a [Flow] of [PagingData].
- * @param key a factory of stable and unique keys representing the item. Using the same key
- * for multiple items in the list is not allowed. Type of the key should be saveable
- * via Bundle on Android. If null is passed the position in the list will represent the key.
- * When you specify the key the scroll position will be maintained based on the key, which
- * means if you add/remove items before the current visible item the item with the given key
- * will be kept as the first visible one.
- * @param itemContent the content displayed by a single item. In case the item is `null`, the
- * [itemContent] method should handle the logic of displaying a placeholder instead of the main
- * content displayed by an item which is not `null`.
- */
-public fun LazyListScope.itemsIndexed(
- items: LazyPagingItems,
- key: ((index: Int, item: T) -> Any)? = null,
- itemContent: @Composable LazyItemScope.(index: Int, value: T?) -> Unit
-) {
- items(
- count = items.itemCount,
- key = if (key == null) null else { index ->
- val item = items.peek(index)
- if (item == null) {
- PagingPlaceholderKey(index)
- } else {
- key(index, item)
- }
- }
- ) { index ->
- itemContent(index, items[index])
- }
-}
-
-@SuppressLint("BanParcelableUsage")
-private data class PagingPlaceholderKey(private val index: Int) : Parcelable {
- override fun writeToParcel(parcel: Parcel, flags: Int) {
- parcel.writeInt(index)
- }
-
- override fun describeContents(): Int {
- return 0
- }
-
- companion object {
- @Suppress("unused")
- @JvmField
- val CREATOR: Parcelable.Creator =
- object : Parcelable.Creator {
- override fun createFromParcel(parcel: Parcel) =
- PagingPlaceholderKey(parcel.readInt())
-
- override fun newArray(size: Int) = arrayOfNulls(size)
- }
- }
-}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/TwidereApp.kt b/app/src/main/kotlin/com/twidere/twiderex/TwidereApp.kt
index b0b65dfa2..9fefbffda 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/TwidereApp.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/TwidereApp.kt
@@ -21,9 +21,11 @@
package com.twidere.twiderex
import android.app.Application
+import android.content.Context
import androidx.hilt.work.HiltWorkerFactory
import androidx.startup.AppInitializer
import androidx.work.Configuration
+import com.twidere.twiderex.http.TwidereServiceInitializer
import com.twidere.twiderex.notification.NotificationInitializer
import com.twidere.twiderex.worker.dm.DirectMessageInitializer
import dagger.hilt.android.HiltAndroidApp
@@ -41,11 +43,21 @@ class TwidereApp : Application(), Configuration.Provider {
override fun onCreate() {
super.onCreate()
+ // Note:Installs with missing splits are now blocked on devices which have Play Protect active or run on Android 10.
+ // But there are still some custom roms allows missing splits which causes resources not found exception
+ if (MissingSplitsCheckerImpl().requiredSplits(this)) {
+ return
+ }
// manually setup NotificationInitializer since it require HiltWorkerFactory
AppInitializer.getInstance(this)
.apply {
initializeComponent(NotificationInitializer::class.java)
initializeComponent(DirectMessageInitializer::class.java)
+ initializeComponent(TwidereServiceInitializer::class.java)
}
}
+
+ interface MissingSplitsChecker {
+ fun requiredSplits(context: Context): Boolean
+ }
}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/TwidereXActivity.kt b/app/src/main/kotlin/com/twidere/twiderex/TwidereXActivity.kt
index 82e9e6f08..c2f51c77e 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/TwidereXActivity.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/TwidereXActivity.kt
@@ -30,22 +30,42 @@ import android.os.Bundle
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Scaffold
+import androidx.compose.material.darkColors
+import androidx.compose.material.lightColors
+import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+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.painterResource
+import androidx.compose.ui.res.stringResource
import androidx.core.net.ConnectivityManagerCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsControllerCompat
-import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.compose.viewModel
-import com.google.accompanist.insets.ExperimentalAnimatedInsets
import com.google.accompanist.insets.ProvideWindowInsets
import com.twidere.twiderex.action.LocalStatusActions
import com.twidere.twiderex.action.StatusActions
import com.twidere.twiderex.component.foundation.LocalInAppNotification
import com.twidere.twiderex.di.assisted.ProvideAssistedFactory
+import com.twidere.twiderex.extensions.observeAsState
import com.twidere.twiderex.navigation.Router
import com.twidere.twiderex.notification.InAppNotification
import com.twidere.twiderex.preferences.PreferencesHolder
@@ -62,6 +82,7 @@ import com.twidere.twiderex.utils.LocalPlatformResolver
import com.twidere.twiderex.utils.PlatformResolver
import com.twidere.twiderex.viewmodel.ActiveAccountViewModel
import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.flow.MutableStateFlow
import moe.tlaster.precompose.navigation.NavController
import javax.inject.Inject
@@ -71,17 +92,15 @@ class TwidereXActivity : ComponentActivity() {
private val navController by lazy {
NavController()
}
- private val isActiveNetworkMetered = MutableLiveData(false)
+ private val isActiveNetworkMetered = MutableStateFlow(false)
private val networkCallback by lazy {
object : ConnectivityManager.NetworkCallback() {
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
- isActiveNetworkMetered.postValue(
- ConnectivityManagerCompat.isActiveNetworkMetered(
- connectivityManager
- )
+ isActiveNetworkMetered.value = ConnectivityManagerCompat.isActiveNetworkMetered(
+ connectivityManager
)
}
}
@@ -105,54 +124,94 @@ class TwidereXActivity : ComponentActivity() {
@Inject
lateinit var platformResolver: PlatformResolver
- @OptIn(ExperimentalAnimatedInsets::class)
+ @OptIn(ExperimentalAnimationApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- isActiveNetworkMetered.postValue(
- ConnectivityManagerCompat.isActiveNetworkMetered(
- connectivityManager
- )
+ isActiveNetworkMetered.value = ConnectivityManagerCompat.isActiveNetworkMetered(
+ connectivityManager
)
+
WindowCompat.setDecorFitsSystemWindows(window, false)
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
setContent {
- val windowInsetsControllerCompat =
- remember { WindowInsetsControllerCompat(window, window.decorView) }
- val accountViewModel = viewModel()
- val account by accountViewModel.account.observeAsState()
- val isActiveNetworkMetered by isActiveNetworkMetered.observeAsState(initial = false)
- CompositionLocalProvider(
- LocalInAppNotification provides inAppNotification,
- LocalWindow provides window,
- LocalWindowInsetsController provides windowInsetsControllerCompat,
- LocalActiveAccount provides account,
- LocalApplication provides application,
- LocalStatusActions provides statusActions,
- LocalActivity provides this,
- LocalActiveAccountViewModel provides accountViewModel,
- LocalIsActiveNetworkMetered provides isActiveNetworkMetered,
- LocalPlatformResolver provides platformResolver,
+ var showSplash by rememberSaveable { mutableStateOf(true) }
+ LaunchedEffect(Unit) {
+ preferencesHolder.warmup()
+ showSplash = false
+ }
+ App()
+ AnimatedVisibility(
+ visible = showSplash,
+ enter = fadeIn(),
+ exit = fadeOut(),
+ ) {
+ Splash()
+ }
+ }
+ intent.data?.let {
+ onDeeplink(it)
+ }
+ }
+
+ @Composable
+ private fun Splash() {
+ MaterialTheme(
+ colors = if (isSystemInDarkTheme()) {
+ darkColors()
+ } else {
+ lightColors()
+ }
+ ) {
+ Scaffold {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Image(
+ painter = painterResource(id = R.drawable.ic_login_logo),
+ contentDescription = stringResource(id = R.string.accessibility_common_logo_twidere)
+ )
+ }
+ }
+ }
+ }
+
+ @Composable
+ private fun App() {
+ val windowInsetsControllerCompat =
+ remember { WindowInsetsControllerCompat(window, window.decorView) }
+ val accountViewModel = viewModel()
+ val account by accountViewModel.account.observeAsState(null)
+ val isActiveNetworkMetered by isActiveNetworkMetered.observeAsState(initial = false)
+ CompositionLocalProvider(
+ LocalInAppNotification provides inAppNotification,
+ LocalWindow provides window,
+ LocalWindowInsetsController provides windowInsetsControllerCompat,
+ LocalActiveAccount provides account,
+ LocalApplication provides application,
+ LocalStatusActions provides statusActions,
+ LocalActivity provides this,
+ LocalActiveAccountViewModel provides accountViewModel,
+ LocalIsActiveNetworkMetered provides isActiveNetworkMetered,
+ LocalPlatformResolver provides platformResolver,
+ ) {
+ ProvidePreferences(
+ preferencesHolder,
) {
- ProvidePreferences(
- preferencesHolder,
+ ProvideAssistedFactory(
+ viewModelHolder.factory,
) {
- ProvideAssistedFactory(
- viewModelHolder.factory,
+ ProvideWindowInsets(
+ windowInsetsAnimationsEnabled = true
) {
- ProvideWindowInsets(
- windowInsetsAnimationsEnabled = true
- ) {
- Router(
- navController = navController
- )
- }
+ Router(
+ navController = navController
+ )
}
}
}
}
- intent.data?.let {
- onDeeplink(it)
- }
}
private fun onDeeplink(it: Uri) {
diff --git a/app/src/main/kotlin/com/twidere/twiderex/action/ComposeAction.kt b/app/src/main/kotlin/com/twidere/twiderex/action/ComposeAction.kt
index 150826b08..fa671a1a7 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/action/ComposeAction.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/action/ComposeAction.kt
@@ -21,9 +21,9 @@
package com.twidere.twiderex.action
import androidx.work.WorkManager
-import com.twidere.twiderex.model.ComposeData
import com.twidere.twiderex.model.MicroBlogKey
-import com.twidere.twiderex.model.PlatformType
+import com.twidere.twiderex.model.enums.PlatformType
+import com.twidere.twiderex.model.job.ComposeData
import com.twidere.twiderex.worker.compose.MastodonComposeWorker
import com.twidere.twiderex.worker.compose.TwitterComposeWorker
import com.twidere.twiderex.worker.draft.RemoveDraftWorker
diff --git a/app/src/main/kotlin/com/twidere/twiderex/action/DirectMessageAction.kt b/app/src/main/kotlin/com/twidere/twiderex/action/DirectMessageAction.kt
index 2e2d22ea4..fc66e0d0e 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/action/DirectMessageAction.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/action/DirectMessageAction.kt
@@ -21,9 +21,9 @@
package com.twidere.twiderex.action
import androidx.work.WorkManager
-import com.twidere.twiderex.model.DirectMessageDeleteData
-import com.twidere.twiderex.model.DirectMessageSendData
-import com.twidere.twiderex.model.PlatformType
+import com.twidere.twiderex.model.enums.PlatformType
+import com.twidere.twiderex.model.job.DirectMessageDeleteData
+import com.twidere.twiderex.model.job.DirectMessageSendData
import com.twidere.twiderex.worker.dm.DirectMessageDeleteWorker
import com.twidere.twiderex.worker.dm.TwitterDirectMessageSendWorker
diff --git a/app/src/main/kotlin/com/twidere/twiderex/action/StatusActions.kt b/app/src/main/kotlin/com/twidere/twiderex/action/StatusActions.kt
index 013e2609a..bc3290400 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/action/StatusActions.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/action/StatusActions.kt
@@ -23,13 +23,13 @@ package com.twidere.twiderex.action
import androidx.compose.runtime.compositionLocalOf
import androidx.work.WorkManager
import com.twidere.twiderex.model.AccountDetails
+import com.twidere.twiderex.model.job.StatusResult
import com.twidere.twiderex.model.ui.UiStatus
import com.twidere.twiderex.worker.database.DeleteDbStatusWorker
import com.twidere.twiderex.worker.status.DeleteStatusWorker
import com.twidere.twiderex.worker.status.LikeWorker
import com.twidere.twiderex.worker.status.MastodonVoteWorker
import com.twidere.twiderex.worker.status.RetweetWorker
-import com.twidere.twiderex.worker.status.StatusResult
import com.twidere.twiderex.worker.status.StatusWorker
import com.twidere.twiderex.worker.status.UnLikeWorker
import com.twidere.twiderex.worker.status.UnRetweetWorker
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/LoginLogo.kt b/app/src/main/kotlin/com/twidere/twiderex/component/LoginLogo.kt
index 1f2b43407..234fec77b 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/component/LoginLogo.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/LoginLogo.kt
@@ -27,21 +27,15 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import com.twidere.twiderex.R
-import com.twidere.twiderex.extensions.isDarkTheme
@Composable
fun LoginLogo(
modifier: Modifier = Modifier,
) {
- val resource = if (isDarkTheme()) {
- painterResource(id = R.drawable.ic_login_logo_dark)
- } else {
- painterResource(id = R.drawable.ic_login_logo)
- }
Image(
modifier = modifier,
contentScale = ContentScale.FillWidth,
- painter = resource,
+ painter = painterResource(id = R.drawable.ic_login_logo),
contentDescription = stringResource(id = R.string.accessibility_common_logo_twidere)
)
}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/TimelineComponent.kt b/app/src/main/kotlin/com/twidere/twiderex/component/TimelineComponent.kt
index 30f8d39c5..7e49de071 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/component/TimelineComponent.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/TimelineComponent.kt
@@ -25,7 +25,6 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.unit.dp
@@ -34,6 +33,7 @@ import androidx.paging.compose.collectAsLazyPagingItems
import com.twidere.twiderex.component.foundation.SwipeToRefreshLayout
import com.twidere.twiderex.component.lazy.LazyListController
import com.twidere.twiderex.component.lazy.ui.LazyUiStatusList
+import com.twidere.twiderex.extensions.observeAsState
import com.twidere.twiderex.extensions.refreshOrRetry
import com.twidere.twiderex.viewmodel.timeline.TimelineScrollState
import com.twidere.twiderex.viewmodel.timeline.TimelineViewModel
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/UserComponent.kt b/app/src/main/kotlin/com/twidere/twiderex/component/UserComponent.kt
index c563821e3..74f768fc4 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/component/UserComponent.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/UserComponent.kt
@@ -62,7 +62,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
-import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@@ -98,13 +97,14 @@ import com.twidere.twiderex.component.status.UserAvatar
import com.twidere.twiderex.component.status.UserName
import com.twidere.twiderex.component.status.UserScreenName
import com.twidere.twiderex.component.status.withAvatarClip
-import com.twidere.twiderex.db.model.TwitterUrlEntity
import com.twidere.twiderex.di.assisted.assistedViewModel
+import com.twidere.twiderex.extensions.observeAsState
import com.twidere.twiderex.extensions.withElevation
import com.twidere.twiderex.model.MicroBlogKey
-import com.twidere.twiderex.model.PlatformType
+import com.twidere.twiderex.model.enums.PlatformType
+import com.twidere.twiderex.model.ui.UiUrlEntity
import com.twidere.twiderex.model.ui.UiUser
-import com.twidere.twiderex.navigation.Route
+import com.twidere.twiderex.navigation.RootRoute
import com.twidere.twiderex.navigation.twidereXSchema
import com.twidere.twiderex.ui.LocalActiveAccount
import com.twidere.twiderex.ui.LocalNavController
@@ -275,15 +275,15 @@ private fun UserStatusTimelineFilter(
Spacer(modifier = Modifier.width(UserStatusTimelineFilterDefaults.StartSpacing))
Text(
modifier = Modifier.weight(1f),
- text = if (user.statusesCount > 1) {
+ text = if (user.metrics.status > 1) {
stringResource(
id = R.string.common_countable_tweet_single,
- user.statusesCount
+ user.metrics.status
)
} else {
stringResource(
id = R.string.common_countable_tweet_multiple,
- user.statusesCount
+ user.metrics.status
)
}
)
@@ -400,7 +400,7 @@ val maxBannerSize = 200.dp
fun UserInfo(
viewModel: UserViewModel,
) {
- val user by viewModel.user.observeAsState()
+ val user by viewModel.user.observeAsState(initial = null)
val navController = LocalNavController.current
Box(
modifier = Modifier
@@ -441,7 +441,7 @@ fun UserInfo(
size = UserInfoDefaults.AvatarSize
) {
if (user.profileImage is String) {
- navController.navigate(Route.Media.Raw(user.profileImage))
+ navController.navigate(RootRoute.Media.Raw(user.profileImage))
}
}
}
@@ -702,7 +702,7 @@ private fun UserBanner(
.heightIn(max = maxBannerSize)
.clickable(
onClick = {
- navController.navigate(Route.Media.Raw(bannerUrl))
+ navController.navigate(RootRoute.Media.Raw(bannerUrl))
},
indication = null,
interactionSource = remember { MutableInteractionSource() },
@@ -730,9 +730,9 @@ fun UserMetrics(
modifier = Modifier
.weight(1f)
.clickable {
- navController.navigate(Route.Following(user.userKey))
+ navController.navigate(RootRoute.Following(user.userKey))
},
- primaryText = user.friendsCount.toString(),
+ primaryText = user.metrics.follow.toString(),
secondaryText = stringResource(id = R.string.common_controls_profile_dashboard_following),
)
HorizontalDivider(
@@ -742,9 +742,9 @@ fun UserMetrics(
modifier = Modifier
.weight(1f)
.clickable {
- navController.navigate(Route.Followers(user.userKey))
+ navController.navigate(RootRoute.Followers(user.userKey))
},
- primaryText = user.followersCount.toString(),
+ primaryText = user.metrics.fans.toString(),
secondaryText = stringResource(id = R.string.common_controls_profile_dashboard_followers),
)
if (user.platformType == PlatformType.Twitter) {
@@ -754,7 +754,7 @@ fun UserMetrics(
MetricsItem(
modifier = Modifier
.weight(1f),
- primaryText = user.listedCount.toString(),
+ primaryText = user.metrics.listed.toString(),
secondaryText = stringResource(id = R.string.common_controls_profile_dashboard_listed),
)
}
@@ -780,7 +780,7 @@ fun MetricsItem(
fun UserDescText(
modifier: Modifier = Modifier,
htmlDesc: String,
- url: List,
+ url: List,
) {
key(
htmlDesc,
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/foundation/BlurImage.kt b/app/src/main/kotlin/com/twidere/twiderex/component/foundation/BlurImage.kt
index 910fe48d3..873db16b7 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/component/foundation/BlurImage.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/foundation/BlurImage.kt
@@ -47,7 +47,6 @@ import androidx.core.graphics.drawable.toDrawable
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
import coil.ImageLoader
import coil.bitmap.BitmapPool
-import coil.compose.LocalImageLoader
import coil.compose.rememberImagePainter
import coil.memory.MemoryCache
import coil.request.DefaultRequestOptions
@@ -55,6 +54,8 @@ import coil.request.Disposable
import coil.request.ImageRequest
import coil.request.ImageResult
import coil.request.SuccessResult
+import com.twidere.twiderex.http.TwidereNetworkImageLoader
+import com.twidere.twiderex.ui.LocalActiveAccount
@Composable
fun BlurImage(
@@ -98,13 +99,18 @@ fun NetworkBlurImage(
placeholder: @Composable (() -> Unit)? = null,
) {
val context = LocalContext.current
+ val accountDetails = LocalActiveAccount.current
val painter = rememberImagePainter(
data = data,
imageLoader = BlurImageLoader(
context,
blurRadius,
bitmapScale,
- LocalImageLoader.current
+ TwidereNetworkImageLoader(
+ realImageLoader = buildRealImageLoader(),
+ context = context,
+ account = accountDetails
+ )
)
)
NetworkImage(
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/foundation/InAppNotificationScaffold.kt b/app/src/main/kotlin/com/twidere/twiderex/component/foundation/InAppNotificationScaffold.kt
index a346eb478..d67fe4ef4 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/component/foundation/InAppNotificationScaffold.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/foundation/InAppNotificationScaffold.kt
@@ -42,7 +42,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -61,7 +60,7 @@ fun ApplyNotification(
snackbarHostState: SnackbarHostState
) {
val inAppNotification = LocalInAppNotification.current
- val notification by inAppNotification.observeAsState()
+ val notification by inAppNotification.observeAsState(null)
val event = notification?.getContentIfNotHandled()
val message = event?.getMessage()
val actionMessage = event?.let {
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/foundation/NetworkImage.kt b/app/src/main/kotlin/com/twidere/twiderex/component/foundation/NetworkImage.kt
index d80415885..93284077d 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/component/foundation/NetworkImage.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/foundation/NetworkImage.kt
@@ -20,8 +20,7 @@
*/
package com.twidere.twiderex.component.foundation
-import android.content.Context
-import android.net.Uri
+import android.os.Build
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -31,24 +30,17 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import coil.ImageLoader
import coil.annotation.ExperimentalCoilApi
-import coil.bitmap.BitmapPool
import coil.compose.ImagePainter
import coil.compose.LocalImageLoader
import coil.compose.rememberImagePainter
-import coil.memory.MemoryCache
-import coil.request.DefaultRequestOptions
-import coil.request.Disposable
-import coil.request.ImageRequest
-import coil.request.ImageResult
-import com.twidere.services.http.authorization.OAuth1Authorization
+import coil.decode.GifDecoder
+import coil.decode.ImageDecoderDecoder
+import coil.util.CoilUtils
import com.twidere.twiderex.R
-import com.twidere.twiderex.model.AccountDetails
-import com.twidere.twiderex.model.PlatformType
-import com.twidere.twiderex.model.cred.OAuthCredentials
+import com.twidere.twiderex.http.TwidereNetworkImageLoader
+import com.twidere.twiderex.http.TwidereServiceFactory
+import com.twidere.twiderex.preferences.LocalHttpConfig
import com.twidere.twiderex.ui.LocalActiveAccount
-import okhttp3.Headers
-import okhttp3.Request
-import java.net.URL
@OptIn(ExperimentalCoilApi::class)
@Composable
@@ -63,8 +55,8 @@ fun NetworkImage(
} else {
rememberImagePainter(
data = data,
- imageLoader = TwidereImageLoader(
- LocalImageLoader.current,
+ imageLoader = TwidereNetworkImageLoader(
+ buildRealImageLoader(),
LocalContext.current,
LocalActiveAccount.current
),
@@ -84,60 +76,33 @@ fun NetworkImage(
)
}
-private class TwidereImageLoader(
- private val realImageLoader: ImageLoader,
- private val context: Context,
- private val account: AccountDetails?
-) : ImageLoader {
- private val twitterTonApiHost = "ton.twitter.com"
- override val bitmapPool: BitmapPool
- get() = realImageLoader.bitmapPool
- override val defaults: DefaultRequestOptions
- get() = realImageLoader.defaults
- override val memoryCache: MemoryCache
- get() = realImageLoader.memoryCache
-
- override fun enqueue(request: ImageRequest): Disposable {
- return realImageLoader.enqueue(handleRequest(request))
- }
-
- override suspend fun execute(request: ImageRequest): ImageResult {
- return realImageLoader.execute(handleRequest(request))
- }
-
- override fun newBuilder(): ImageLoader.Builder {
- return ImageLoader.Builder(context)
- }
-
- override fun shutdown() {
- realImageLoader.shutdown()
- }
-
- private fun handleRequest(request: ImageRequest): ImageRequest {
- var data = request.data
- // ton.twitter.com must be retrieved via an authenticated
- if (data is String) data = Uri.parse(data)
- return if (data is Uri && twitterTonApiHost == data.host && account?.type == PlatformType.Twitter) {
- val auth = (account.credentials as OAuthCredentials).let {
- OAuth1Authorization(
- consumerKey = it.consumer_key,
- consumerSecret = it.consumer_secret,
- accessToken = it.access_token,
- accessSecret = it.access_token_secret,
- )
- }
- request.newBuilder(
- request.context
- ).headers(
- headers = Headers.headersOf(
- "Authorization",
- auth.getAuthorizationHeader(Request.Builder().url(URL(data.toString())).build())
+@Composable
+fun buildRealImageLoader(): ImageLoader {
+ val context = LocalContext.current
+ val httpConfig = LocalHttpConfig.current
+ return (
+ if (httpConfig.proxyConfig.enable &&
+ httpConfig.proxyConfig.server.isNotEmpty()
+ ) {
+ LocalImageLoader.current
+ .newBuilder()
+ .callFactory(
+ TwidereServiceFactory.createHttpClientFactory()
+ .createHttpClientBuilder()
+ .cache(CoilUtils.createDefaultCache(context))
+ .build()
)
- ).build()
- } else {
- request.newBuilder(request.context)
- .data(data)
.build()
+ } else {
+ LocalImageLoader.current
}
- }
+ ).newBuilder()
+ .componentRegistry {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ add(ImageDecoderDecoder(context))
+ } else {
+ add(GifDecoder())
+ }
+ }
+ .build()
}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/foundation/Pager.kt b/app/src/main/kotlin/com/twidere/twiderex/component/foundation/Pager.kt
index 5caa64974..963655475 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/component/foundation/Pager.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/foundation/Pager.kt
@@ -20,6 +20,7 @@
*/
package com.twidere.twiderex.component.foundation
+import androidx.annotation.IntRange
import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.awaitHorizontalTouchSlopOrCancellation
@@ -38,7 +39,6 @@ import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
-import androidx.compose.runtime.structuralEqualityPolicy
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
@@ -65,59 +65,67 @@ import kotlin.math.withSign
@Composable
fun rememberPagerState(
- currentPage: Int = 0,
- minPage: Int = 0,
- maxPage: Int = 0,
+ @IntRange(from = 0) pageCount: Int,
+ @IntRange(from = 0) initialPage: Int = 0,
): PagerState {
return rememberSaveable(
saver = PagerState.Saver(),
) {
- PagerState(currentPage, minPage, maxPage)
+ PagerState(pageCount = pageCount, currentPage = initialPage)
+ }.apply {
+ this.pageCount = pageCount
}
}
@Stable
class PagerState(
- currentPage: Int = 0,
- minPage: Int = 0,
- maxPage: Int = 0,
+ @IntRange(from = 0) pageCount: Int,
+ @IntRange(from = 0) currentPage: Int = 0,
) {
private val velocityTracker = VelocityTracker()
+ private var _pageCount by mutableStateOf(pageCount)
+ private var _currentPage by mutableStateOf(currentPage)
companion object {
fun Saver(): Saver = listSaver(
- save = { listOf(it.currentPage, it.minPage, it.maxPage) },
+ save = { listOf(it.pageCount, it.currentPage) },
restore = {
PagerState(
- currentPage = it[0],
- minPage = it[1],
- maxPage = it[2],
+ pageCount = it[0],
+ currentPage = it[1],
)
}
)
}
- private var _minPage by mutableStateOf(minPage)
- var minPage: Int
- get() = _minPage
- set(value) {
- _minPage = value.coerceAtMost(_maxPage)
- _currentPage = _currentPage.coerceIn(_minPage, _maxPage)
+ internal inline val firstPageIndex: Int
+ get() = 0
+
+ internal inline val lastPageIndex: Int
+ get() = (pageCount - 1).coerceAtLeast(0)
+
+ @get:IntRange(from = 0)
+ var pageCount: Int
+ get() = _pageCount
+ set(@IntRange(from = 0) value) {
+ require(value >= 0) { "pageCount must be >= 0" }
+ _pageCount = value
+ currentPage = currentPage.coerceIn(firstPageIndex, lastPageIndex)
+ // updateLayoutPages(currentPage)
}
- private var _maxPage by mutableStateOf(maxPage, structuralEqualityPolicy())
- var maxPage: Int
- get() = _maxPage
- set(value) {
- _maxPage = value.coerceAtLeast(_minPage)
- _currentPage = _currentPage.coerceIn(_minPage, maxPage)
+ private fun Int.floorMod(other: Int): Int {
+ return when (other) {
+ 0 -> this
+ else -> this - this.floorDiv(other) * other
}
+ }
- private var _currentPage by mutableStateOf(currentPage.coerceIn(minPage, maxPage))
+ @get:IntRange(from = 0)
var currentPage: Int
get() = _currentPage
set(value) {
- _currentPage = value.coerceIn(minPage, maxPage)
+ _currentPage = value.floorMod(pageCount)
}
enum class SelectionState { Selected, Undecided }
@@ -144,14 +152,14 @@ class PagerState(
get() = _currentPageOffset.value
suspend fun snapToOffset(offset: Float) {
- val max = if (currentPage == minPage) 0f else 1f
- val min = if (currentPage == maxPage) 0f else -1f
+ val max = if (currentPage == firstPageIndex) 0f else 1f
+ val min = if (currentPage == lastPageIndex) 0f else -1f
_currentPageOffset.snapTo(offset.coerceIn(min, max))
}
suspend fun fling(velocity: Float) {
- if (velocity < 0 && currentPage == maxPage) return
- if (velocity > 0 && currentPage == minPage) return
+ if (velocity < 0 && currentPage == lastPageIndex) return
+ if (velocity > 0 && currentPage == firstPageIndex) return
val currentOffset = _currentPageOffset.value
when {
currentOffset.sign == velocity.sign &&
@@ -169,9 +177,6 @@ class PagerState(
}
}
- override fun toString(): String = "PagerState{minPage=$minPage, maxPage=$maxPage, " +
- "currentPage=$currentPage, currentPageOffset=$currentPageOffset}"
-
fun addPosition(uptimeMillis: Long, position: Offset) {
velocityTracker.addPosition(timeMillis = uptimeMillis, position = position)
}
@@ -202,8 +207,8 @@ fun Pager(
var pageSize by remember { mutableStateOf(0) }
Layout(
content = {
- val minPage = (state.currentPage - offscreenLimit).coerceAtLeast(state.minPage)
- val maxPage = (state.currentPage + offscreenLimit).coerceAtMost(state.maxPage)
+ val minPage = (state.currentPage - offscreenLimit).coerceAtLeast(state.firstPageIndex)
+ val maxPage = (state.currentPage + offscreenLimit).coerceAtMost(state.lastPageIndex)
for (page in minPage..maxPage) {
val pageData = PageData(page)
@@ -224,9 +229,9 @@ fun Pager(
selectionState = PagerState.SelectionState.Undecided
val pos = pageSize * currentPageOffset
val max =
- if (currentPage == minPage) 0 else pageSize * offscreenLimit
+ if (currentPage == firstPageIndex) 0 else pageSize * offscreenLimit
val min =
- if (currentPage == maxPage) 0 else -pageSize * offscreenLimit
+ if (currentPage == lastPageIndex) 0 else -pageSize * offscreenLimit
val newPos =
(pos + dragAmount).coerceIn(min.toFloat(), max.toFloat())
if (newPos != 0f) {
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/foundation/ReorderableColumn.kt b/app/src/main/kotlin/com/twidere/twiderex/component/foundation/ReorderableColumn.kt
new file mode 100644
index 000000000..48c62ad10
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/foundation/ReorderableColumn.kt
@@ -0,0 +1,225 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.component.foundation
+
+import android.view.HapticFeedbackConstants
+import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.zIndex
+import kotlin.math.roundToInt
+
+@Composable
+fun rememberReorderableColumnState(
+ onReorder: (oldIndex: Int, newIndex: Int) -> Unit
+) = remember {
+ ReorderableColumnState(
+ onReorder = onReorder
+ )
+}
+
+@Stable
+class ReorderableColumnState(
+ private val onReorder: (oldIndex: Int, newIndex: Int) -> Unit,
+) {
+ internal var reordering by mutableStateOf(false)
+ internal var draggingItemIndex: Int = -1
+ internal var newTargetIndex by mutableStateOf(-1)
+ internal var offsetY by mutableStateOf(0f)
+ internal var childSizes = arrayListOf()
+
+ internal fun start(index: Int) {
+ draggingItemIndex = index
+ reordering = true
+ }
+
+ internal fun drag(y: Float) {
+ offsetY += y
+ if (offsetY.roundToInt() == 0) {
+ return
+ }
+
+ val newOffset =
+ (childSizes.subList(0, draggingItemIndex).sumOf { it.height } + offsetY).roundToInt()
+
+ newTargetIndex = ArrayList(childSizes)
+ .apply {
+ removeAt(draggingItemIndex)
+ }
+ .map { it.height }
+ .runningReduce { acc, i -> acc + i }
+ .let { it + newOffset }
+ .sortedBy { it }
+ .indexOf(newOffset)
+ .let {
+ if (offsetY < 0) {
+ it + 1
+ } else {
+ it
+ }
+ }
+ }
+
+ internal fun cancel() {
+ reordering = false
+ draggingItemIndex = -1
+ newTargetIndex = -1
+ offsetY = 0f
+ }
+
+ internal fun drop() {
+ if (offsetY.roundToInt() == 0) {
+ return
+ }
+ val newOffset =
+ (childSizes.subList(0, draggingItemIndex).sumOf { it.height } + offsetY).roundToInt()
+
+ val newIndex = ArrayList(childSizes)
+ .apply {
+ removeAt(draggingItemIndex)
+ }
+ .map { it.height }
+ .runningReduce { acc, i -> acc + i }
+ .let { it + newOffset }
+ .sortedBy { it }
+ .indexOf(newOffset)
+ .let {
+ if (offsetY < 0) {
+ it + 1
+ } else {
+ it
+ }
+ }
+
+ onReorder.invoke(draggingItemIndex, newIndex)
+
+ reordering = false
+ draggingItemIndex = -1
+ newTargetIndex = -1
+ offsetY = 0f
+ }
+}
+
+@Composable
+fun ReorderableColumn(
+ modifier: Modifier = Modifier,
+ data: List,
+ state: ReorderableColumnState,
+ dragingContent: @Composable ((T) -> Unit)? = null,
+ itemContent: @Composable (T) -> Unit,
+) {
+ val view = LocalView.current
+ Layout(
+ modifier = modifier,
+ content = {
+ data.forEachIndexed { index, item ->
+ Box(
+ modifier = Modifier
+ .pointerInput(Unit) {
+ detectDragGesturesAfterLongPress(
+ onDragCancel = {
+ state.cancel()
+ },
+ onDragEnd = {
+ state.drop()
+ },
+ onDragStart = {
+ view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
+ state.start(index)
+ },
+ onDrag = { _, dragAmount ->
+ state.drag(dragAmount.y)
+ }
+ )
+ }
+ .let {
+ if (state.reordering && state.draggingItemIndex == index) {
+ it.zIndex(0.1f)
+ } else {
+ it.zIndex(0f)
+ }
+ }
+ ) {
+ if (state.reordering && state.draggingItemIndex == index && dragingContent != null) {
+ dragingContent.invoke(item)
+ } else {
+ itemContent.invoke(item)
+ }
+ }
+ }
+ }
+ ) { measurables, constraints ->
+ val placeables = measurables.map {
+ it.measure(constraints)
+ }
+ state.childSizes.clear()
+ state.childSizes.addAll(
+ placeables.map {
+ IntSize(
+ width = it.measuredWidth,
+ height = it.measuredHeight
+ )
+ }
+ )
+
+ layout(
+ width = placeables.maxOf { it.measuredWidth },
+ height = placeables.sumOf { it.measuredHeight }
+ ) {
+ var height = 0
+ placeables.forEachIndexed { index, placeable ->
+ if (state.reordering && index == state.newTargetIndex && index != state.draggingItemIndex && state.offsetY < 0) {
+ height += placeables[state.draggingItemIndex].height
+ }
+ if (state.reordering && index == state.draggingItemIndex) {
+ placeable.place(
+ 0,
+ (height + state.offsetY.roundToInt()).let {
+ if (state.newTargetIndex != -1 && index != state.newTargetIndex && state.offsetY < 0) {
+ it - placeables[state.newTargetIndex].height
+ } else {
+ it
+ }
+ }
+ )
+ } else {
+ placeable.place(0, height)
+ }
+ if (state.reordering && index == state.newTargetIndex && index != state.draggingItemIndex && state.offsetY > 0) {
+ height += placeables[state.draggingItemIndex].height
+ }
+ if (!state.reordering || index != state.draggingItemIndex || state.newTargetIndex == -1 || index == state.newTargetIndex) {
+ height += placeable.measuredHeight
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/foundation/VideoPlayer.kt b/app/src/main/kotlin/com/twidere/twiderex/component/foundation/VideoPlayer.kt
index 2ebf72292..3ac33fa92 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/component/foundation/VideoPlayer.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/foundation/VideoPlayer.kt
@@ -51,11 +51,16 @@ import androidx.lifecycle.OnLifecycleEvent
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.SimpleExoPlayer
+import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource
+import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.ui.PlayerControlView
import com.google.android.exoplayer2.ui.StyledPlayerView
+import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
import com.twidere.twiderex.R
import com.twidere.twiderex.component.status.UserAvatarDefaults
+import com.twidere.twiderex.http.TwidereServiceFactory
+import com.twidere.twiderex.preferences.LocalHttpConfig
import com.twidere.twiderex.preferences.proto.DisplayPreferences
import com.twidere.twiderex.ui.LocalIsActiveNetworkMetered
import com.twidere.twiderex.ui.LocalVideoPlayback
@@ -70,6 +75,7 @@ fun VideoPlayer(
customControl: PlayerControlView? = null,
showControls: Boolean = customControl == null,
zOrderMediaOverlay: Boolean = false,
+ keepScreenOn: Boolean = false,
thumb: @Composable (() -> Unit)? = null,
) {
var playing by remember { mutableStateOf(false) }
@@ -85,35 +91,55 @@ fun VideoPlayer(
var autoPlay by remember(url) { mutableStateOf(playInitial) }
val context = LocalContext.current
val lifecycle = LocalLifecycleOwner.current.lifecycle
+ val httpConfig = LocalHttpConfig.current
Box {
if (playInitial) {
val player = remember(url) {
- SimpleExoPlayer.Builder(context).build().apply {
- repeatMode = Player.REPEAT_MODE_ALL
- playWhenReady = autoPlay
- addListener(object : Player.Listener {
- override fun onPlaybackStateChanged(state: Int) {
- shouldShowThumb = state != Player.STATE_READY
+ SimpleExoPlayer.Builder(context)
+ .apply {
+ if (httpConfig.proxyConfig.enable) {
+ // replace DataSource
+ OkHttpDataSource.Factory(
+ TwidereServiceFactory
+ .createHttpClientFactory()
+ .createHttpClientBuilder()
+ .build()
+ )
+ .let {
+ DefaultDataSourceFactory(context, it)
+ }.let {
+ DefaultMediaSourceFactory(it)
+ }.let {
+ setMediaSourceFactory(it)
+ }
}
+ }
+ .build().apply {
+ repeatMode = Player.REPEAT_MODE_ALL
+ playWhenReady = autoPlay
+ addListener(object : Player.Listener {
+ override fun onPlaybackStateChanged(state: Int) {
+ shouldShowThumb = state != Player.STATE_READY
+ }
- override fun onIsPlayingChanged(isPlaying: Boolean) {
- playing = isPlaying
- }
- })
+ override fun onIsPlayingChanged(isPlaying: Boolean) {
+ playing = isPlaying
+ }
+ })
- setVolume(volume)
- ProgressiveMediaSource.Factory(
- CacheDataSourceFactory(
- context,
- 5 * 1024 * 1024,
- )
- ).createMediaSource(MediaItem.fromUri(url)).also {
- setMediaSource(it)
+ setVolume(volume)
+ ProgressiveMediaSource.Factory(
+ CacheDataSourceFactory(
+ context,
+ 5L * 1024L * 1024L,
+ )
+ ).createMediaSource(MediaItem.fromUri(url)).also {
+ setMediaSource(it)
+ }
+ prepare()
+ seekTo(VideoPool.get(url))
}
- prepare()
- seekTo(VideoPool.get(url))
- }
}
fun updateState() {
@@ -157,6 +183,7 @@ fun VideoPlayer(
StyledPlayerView(context).also { playerView ->
(playerView.videoSurfaceView as? SurfaceView)?.setZOrderMediaOverlay(zOrderMediaOverlay)
playerView.useController = showControls
+ playerView.keepScreenOn = keepScreenOn
}
}
) {
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/BackStackEntry.kt b/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/BackStackEntry.kt
index 4e616c6a6..2442d6629 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/BackStackEntry.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/BackStackEntry.kt
@@ -20,6 +20,9 @@
*/
package moe.tlaster.precompose.navigation
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import moe.tlaster.precompose.navigation.route.ComposeRoute
@@ -30,10 +33,44 @@ class BackStackEntry internal constructor(
val pathMap: Map,
val queryString: QueryString? = null,
internal val viewModel: NavControllerViewModel,
-) : ViewModelStoreOwner {
+) : ViewModelStoreOwner, LifecycleOwner {
+ private var destroyAfterTransition = false
+
override fun getViewModelStore(): ViewModelStore {
return viewModel.get(id = id)
}
+
+ private val lifecycleRegistry by lazy {
+ LifecycleRegistry(this)
+ }
+
+ override fun getLifecycle(): Lifecycle {
+ return lifecycleRegistry
+ }
+
+ fun active() {
+ lifecycleRegistry.currentState = Lifecycle.State.RESUMED
+ }
+
+ fun inActive() {
+ if (lifecycleRegistry.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
+ lifecycleRegistry.currentState = Lifecycle.State.STARTED
+ }
+ if (destroyAfterTransition) {
+ destroy()
+ }
+ }
+
+ fun destroy() {
+ if (lifecycleRegistry.currentState.isAtLeast(Lifecycle.State.RESUMED) ||
+ lifecycleRegistry.currentState == Lifecycle.State.INITIALIZED
+ ) {
+ destroyAfterTransition = true
+ } else {
+ lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
+ viewModelStore.clear()
+ }
+ }
}
inline fun BackStackEntry.path(path: String, default: T? = null): T? {
@@ -45,19 +82,19 @@ inline fun BackStackEntry.query(name: String, default: T? = null): T
return queryString?.query(name, default)
}
-inline fun BackStackEntry.queryList(name: String): List {
+inline fun BackStackEntry.queryList(name: String): List {
val value = queryString?.map?.get(name) ?: return emptyList()
return value.map { convertValue(it) }
}
-inline fun convertValue(value: String): T {
+inline fun convertValue(value: String): T? {
return when (T::class) {
- Int::class -> value.toInt()
- Long::class -> value.toLong()
+ Int::class -> value.toIntOrNull()
+ Long::class -> value.toLongOrNull()
String::class -> value
- Boolean::class -> value.toBoolean()
- Float::class -> value.toFloat()
- Double::class -> value.toDouble()
+ Boolean::class -> value.toBooleanStrictOrNull()
+ Float::class -> value.toFloatOrNull()
+ Double::class -> value.toDoubleOrNull()
else -> throw NotImplementedError()
} as T
}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/NavControllerViewModel.kt b/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/NavControllerViewModel.kt
index e0a12e6e3..bed58e02c 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/NavControllerViewModel.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/NavControllerViewModel.kt
@@ -49,7 +49,7 @@ internal class NavControllerViewModel : ViewModel() {
return ViewModelProvider(
viewModelStore,
object : ViewModelProvider.Factory {
- override fun create(modelClass: Class): T {
+ override fun create(modelClass: Class): T {
@Suppress("UNCHECKED_CAST")
return NavControllerViewModel() as T
}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/NavHost.kt b/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/NavHost.kt
index cc586deb9..1a9ccae8a 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/NavHost.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/NavHost.kt
@@ -21,21 +21,17 @@
package moe.tlaster.precompose.navigation
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
-import androidx.compose.animation.Crossfade
-import androidx.compose.foundation.gestures.forEachGesture
-import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.input.pointer.consumeAllChanges
-import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
+import moe.tlaster.precompose.navigation.transition.AnimatedDialogRoute
import moe.tlaster.precompose.navigation.transition.AnimatedRoute
+import moe.tlaster.precompose.navigation.transition.DialogTransition
import moe.tlaster.precompose.navigation.transition.NavTransition
/**
@@ -57,6 +53,7 @@ fun NavHost(
navController: NavController,
initialRoute: String,
navTransition: NavTransition = remember { NavTransition() },
+ dialogTransition: DialogTransition = remember { DialogTransition() },
builder: RouteBuilder.() -> Unit,
) {
val stateHolder = rememberSaveableStateHolder()
@@ -101,34 +98,27 @@ fun NavHost(
routeStack.onInActive()
}
}
- CompositionLocalProvider(
- LocalLifecycleOwner provides routeStack,
+ val currentEntry = routeStack.currentEntry
+ if (currentEntry != null) {
+ LaunchedEffect(currentEntry) {
+ currentEntry.active()
+ }
+ DisposableEffect(currentEntry) {
+ onDispose {
+ currentEntry.inActive()
+ }
+ }
+ }
+ AnimatedDialogRoute(
+ stack = routeStack,
+ dialogTransition = dialogTransition,
) {
- stateHolder.SaveableStateProvider(routeStack.id) {
+ stateHolder.SaveableStateProvider(it.id) {
CompositionLocalProvider(
- LocalViewModelStoreOwner provides routeStack.scene
+ LocalViewModelStoreOwner provides it,
+ LocalLifecycleOwner provides it,
) {
- routeStack.scene.route.content.invoke(routeStack.scene)
- }
- Crossfade(targetState = routeStack.currentDialogStack) {
- it?.let { backStackEntry ->
- CompositionLocalProvider(
- LocalViewModelStoreOwner provides backStackEntry
- ) {
- Box(
- modifier = Modifier
- .pointerInput(Unit) {
- forEachGesture {
- awaitPointerEventScope {
- awaitPointerEvent().changes.forEach { it.consumeAllChanges() }
- }
- }
- }
- ) {
- backStackEntry.route.content.invoke(backStackEntry)
- }
- }
- }
+ it.route.content.invoke(it)
}
}
}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/QueryString.kt b/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/QueryString.kt
index b4a9ec981..a93ddf77d 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/QueryString.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/QueryString.kt
@@ -43,7 +43,7 @@ inline fun QueryString.query(name: String, default: T? = null): T? {
return convertValue(value)
}
-inline fun QueryString.queryList(name: String): List {
+inline fun QueryString.queryList(name: String): List {
val value = map[name] ?: return emptyList()
return value.map { convertValue(it) }
}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/RouteStack.kt b/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/RouteStack.kt
index 5eca1a6ab..27d79a163 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/RouteStack.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/RouteStack.kt
@@ -23,69 +23,50 @@ package moe.tlaster.precompose.navigation
import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.snapshots.SnapshotStateList
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.LifecycleRegistry
import moe.tlaster.precompose.navigation.transition.NavTransition
@Stable
internal class RouteStack(
val id: Long,
- val scene: BackStackEntry,
- val dialogStack: SnapshotStateList = mutableStateListOf(),
+ val stacks: SnapshotStateList = mutableStateListOf(),
val navTransition: NavTransition? = null,
-) : LifecycleOwner {
+) {
private var destroyAfterTransition = false
- val currentEntry: BackStackEntry
- get() = if (dialogStack.any()) {
- dialogStack.last()
- } else {
- scene
- }
- val currentDialogStack: BackStackEntry?
- get() = dialogStack.lastOrNull()
-
- private val lifecycleRegistry by lazy {
- LifecycleRegistry(this)
- }
+ val currentEntry: BackStackEntry?
+ get() = stacks.lastOrNull()
val canGoBack: Boolean
- get() = dialogStack.isNotEmpty()
+ get() = stacks.size > 1
fun goBack(): BackStackEntry {
- return dialogStack.removeLast().apply {
- viewModelStore.clear()
+ return stacks.removeLast().also {
+ it.destroy()
}
}
fun onActive() {
- lifecycleRegistry.currentState = Lifecycle.State.RESUMED
+ currentEntry?.active()
}
fun onInActive() {
- if (lifecycleRegistry.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
- lifecycleRegistry.currentState = Lifecycle.State.STARTED
- }
+ currentEntry?.inActive()
if (destroyAfterTransition) {
onDestroyed()
}
}
+ fun destroyAfterTransition() {
+ destroyAfterTransition = true
+ }
+
fun onDestroyed() {
- if (lifecycleRegistry.currentState.isAtLeast(Lifecycle.State.RESUMED) ||
- lifecycleRegistry.currentState == Lifecycle.State.INITIALIZED
- ) {
- destroyAfterTransition = true
- } else {
- lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
- dialogStack.forEach {
- it.viewModelStore.clear()
- }
- scene.viewModelStore.clear()
+ stacks.forEach {
+ it.destroy()
}
+ stacks.clear()
}
- override fun getLifecycle(): Lifecycle {
- return lifecycleRegistry
+ fun hasRoute(route: String): Boolean {
+ return stacks.any { it.route.route == route }
}
}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/RouteStackManager.kt b/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/RouteStackManager.kt
index 8510b4d95..8c1aa3f1a 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/RouteStackManager.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/RouteStackManager.kt
@@ -108,7 +108,7 @@ internal class RouteStackManager(
checkNotNull(matchResult) { "RouteStackManager: navigate target $path not found" }
require(matchResult.route is ComposeRoute) { "RouteStackManager: navigate target $path is not ComposeRoute" }
if (options != null && matchResult.route is SceneRoute && options.launchSingleTop) {
- _backStacks.firstOrNull { it.scene.route.route == matchResult.route.route }?.let {
+ _backStacks.firstOrNull { it.hasRoute(matchResult.route.route) }?.let {
_backStacks.remove(it)
_backStacks.add(it)
}
@@ -127,19 +127,19 @@ internal class RouteStackManager(
_backStacks.add(
RouteStack(
id = routeStackId++,
- scene = entry,
+ stacks = mutableStateListOf(entry),
navTransition = matchResult.route.navTransition,
)
)
}
is DialogRoute -> {
- currentStack?.dialogStack?.add(entry)
+ currentStack?.stacks?.add(entry)
}
}
}
if (options?.popUpTo != null && matchResult.route is SceneRoute) {
- val index = _backStacks.indexOfLast { it.scene.route.route == options.popUpTo.route }
+ val index = _backStacks.indexOfLast { it.hasRoute(options.popUpTo.route) }
if (index != -1 && index != _backStacks.lastIndex) {
_backStacks.removeRange(
if (options.popUpTo.inclusive) index else index + 1,
@@ -164,9 +164,10 @@ internal class RouteStackManager(
}
_backStacks.size > 1 -> {
val stack = _backStacks.removeLast()
+ val entry = stack.currentEntry
stateHolder.removeState(stack.id)
- stack.onDestroyed()
- stack.scene
+ stack.destroyAfterTransition()
+ entry
}
else -> {
null
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/transition/AnimatedDialogRoute.kt b/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/transition/AnimatedDialogRoute.kt
new file mode 100644
index 000000000..3ec5c208d
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/transition/AnimatedDialogRoute.kt
@@ -0,0 +1,129 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package moe.tlaster.precompose.navigation.transition
+
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.core.updateTransition
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.lifecycle.Lifecycle
+import moe.tlaster.precompose.navigation.BackStackEntry
+import moe.tlaster.precompose.navigation.RouteStack
+
+@Composable
+internal fun AnimatedDialogRoute(
+ stack: RouteStack,
+ modifier: Modifier = Modifier,
+ animationSpec: FiniteAnimationSpec = tween(),
+ dialogTransition: DialogTransition = remember { DialogTransition() },
+ content: @Composable (BackStackEntry) -> Unit
+) {
+
+ val items = remember { mutableStateListOf>() }
+ val stacks = stack.stacks
+ val targetState = remember(stack.stacks.size) {
+ stack.currentEntry
+ }
+ val transitionState = remember { MutableTransitionState(targetState) }
+ val targetChanged = (targetState != transitionState.targetState)
+ val previousState = transitionState.targetState
+ transitionState.targetState = targetState
+ val transition = updateTransition(transitionState, label = "AnimatedDialogRouteTransition")
+
+ if (targetChanged || items.isEmpty()) {
+ val indexOfNew = stacks.indexOf(targetState).takeIf { it >= 0 } ?: Int.MAX_VALUE
+ val indexOfOld = stacks.indexOf(previousState)
+ .takeIf {
+ it >= 0 ||
+ // Workaround for navOptions
+ targetState?.lifecycle?.currentState == Lifecycle.State.INITIALIZED &&
+ previousState?.lifecycle?.currentState == Lifecycle.State.RESUMED
+ } ?: Int.MAX_VALUE
+ // Only manipulate the list when the state is changed, or in the first run.
+ val keys = items.map {
+ val type = if (indexOfNew >= indexOfOld) AnimateType.Pause else AnimateType.Destroy
+ it.key to type
+ }.toMap().run {
+ toMutableMap().also {
+ val type = if (indexOfNew >= indexOfOld) {
+ AnimateType.Create
+ } else {
+ AnimateType.Resume
+ }
+ if (targetState != null) {
+ it[targetState] = type
+ }
+ }
+ }
+ items.clear()
+ keys.mapTo(items) { (key, value) ->
+ AnimatedRouteItem(key, value) {
+ val factor by transition.animateFloat(
+ transitionSpec = { animationSpec }
+ ) { if (it == key) 1f else 0f }
+ Box(
+ Modifier.graphicsLayer {
+ when (value) {
+ AnimateType.Create -> dialogTransition.createTransition.invoke(
+ this,
+ factor
+ )
+ AnimateType.Destroy -> dialogTransition.destroyTransition.invoke(
+ this,
+ factor
+ )
+ else -> Unit
+ }
+ }
+ ) {
+ content(key)
+ }
+ }
+ }.sortByDescending { it.animateType }
+ } else if (transitionState.currentState == transitionState.targetState) {
+ // Remove all the intermediate items from the list once the animation is finished.
+ items.removeAll { it.animateType == AnimateType.Destroy }
+ }
+
+ Box(modifier) {
+ for (index in items.indices) {
+ val item = items[index]
+ key(item.key) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ ) {
+ item.content.invoke()
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/transition/AnimatedRoute.kt b/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/transition/AnimatedRoute.kt
index 7f039ea7d..da60842fc 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/transition/AnimatedRoute.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/transition/AnimatedRoute.kt
@@ -59,8 +59,8 @@ internal fun AnimatedRoute(
.takeIf {
it >= 0 ||
// Workaround for navOptions
- targetState.lifecycle.currentState == Lifecycle.State.INITIALIZED &&
- previousState.lifecycle.currentState == Lifecycle.State.RESUMED
+ targetState.currentEntry?.lifecycle?.currentState == Lifecycle.State.INITIALIZED &&
+ previousState.currentEntry?.lifecycle?.currentState == Lifecycle.State.RESUMED
} ?: Int.MAX_VALUE
val actualNavTransition = run {
if (indexOfNew >= indexOfOld) targetState else previousState
@@ -118,14 +118,14 @@ internal fun AnimatedRoute(
}
}
-private enum class AnimateType {
+internal enum class AnimateType {
Create,
Destroy,
Pause,
Resume,
}
-private data class AnimatedRouteItem(
+internal data class AnimatedRouteItem(
val key: T,
val animateType: AnimateType,
val content: @Composable () -> Unit
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/transition/DialogTransition.kt b/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/transition/DialogTransition.kt
new file mode 100644
index 000000000..ec5a425ee
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/foundation/navigation/transition/DialogTransition.kt
@@ -0,0 +1,41 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package moe.tlaster.precompose.navigation.transition
+
+import androidx.compose.ui.graphics.GraphicsLayerScope
+
+val fadeCreateTransition: GraphicsLayerScope.(factor: Float) -> Unit = { factor ->
+ alpha = factor
+}
+val fadeDestroyTransition: GraphicsLayerScope.(factor: Float) -> Unit = { factor ->
+ alpha = factor
+}
+
+data class DialogTransition(
+ /**
+ * Transition the scene that about to appear for the first time, similar to activity onCreate, factor from 0.0 to 1.0
+ */
+ val createTransition: GraphicsLayerScope.(factor: Float) -> Unit = fadeCreateTransition,
+ /**
+ * Transition the scene that about to disappear forever, similar to activity onDestroy, factor from 1.0 to 0.0
+ */
+ val destroyTransition: GraphicsLayerScope.(factor: Float) -> Unit = fadeDestroyTransition,
+)
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/lazy/ui/LazyUiDMEventList.kt b/app/src/main/kotlin/com/twidere/twiderex/component/lazy/ui/LazyUiDMEventList.kt
index 4cb9cbe7a..5d5a833fa 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/component/lazy/ui/LazyUiDMEventList.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/lazy/ui/LazyUiDMEventList.kt
@@ -80,7 +80,7 @@ import com.twidere.twiderex.component.status.UserAvatarDefaults
import com.twidere.twiderex.db.model.DbDMEvent
import com.twidere.twiderex.model.ui.UiDMEvent
import com.twidere.twiderex.model.ui.UiMedia
-import com.twidere.twiderex.navigation.Route
+import com.twidere.twiderex.navigation.RootRoute
import com.twidere.twiderex.preferences.proto.DisplayPreferences
import com.twidere.twiderex.ui.LocalNavController
import com.twidere.twiderex.ui.LocalVideoPlayback
@@ -246,7 +246,7 @@ private fun MessageBody(event: UiDMEvent, onItemLongClick: (event: UiDMEvent) ->
MediaMessage(
media = event.media.firstOrNull(),
onClick = {
- navController.navigate(Route.Media.Pure(event.messageKey))
+ navController.navigate(RootRoute.Media.Pure(event.messageKey, 0))
}
)
if (event.media.isNotEmpty() && event.htmlText.isNotEmpty()) Spacer(modifier = Modifier.height(MessageBodyDefaults.ContentSpacing))
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/lazy/ui/LazyUiListsList.kt b/app/src/main/kotlin/com/twidere/twiderex/component/lazy/ui/LazyUiListsList.kt
index 8ecc22a55..0492763d6 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/component/lazy/ui/LazyUiListsList.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/lazy/ui/LazyUiListsList.kt
@@ -54,7 +54,7 @@ import androidx.paging.compose.items
import com.twidere.twiderex.R
import com.twidere.twiderex.component.lazy.loadState
import com.twidere.twiderex.component.status.StatusDivider
-import com.twidere.twiderex.model.ListType
+import com.twidere.twiderex.model.enums.ListType
import com.twidere.twiderex.model.ui.UiList
import moe.tlaster.placeholder.TextPlaceHolder
import java.util.Locale
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/lazy/ui/LazyUiUserList.kt b/app/src/main/kotlin/com/twidere/twiderex/component/lazy/ui/LazyUiUserList.kt
index 9bcf96235..b2f9a00be 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/component/lazy/ui/LazyUiUserList.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/lazy/ui/LazyUiUserList.kt
@@ -101,7 +101,7 @@ fun LazyUiUserList(
)
Spacer(modifier = Modifier.width(UiUserListDefaults.HorizontalPadding))
Text(
- text = it.followersCount.toString()
+ text = it.metrics.fans.toString()
)
}
},
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/navigation/Navigator.kt b/app/src/main/kotlin/com/twidere/twiderex/component/navigation/Navigator.kt
index a92ba5974..cde7e3520 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/component/navigation/Navigator.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/navigation/Navigator.kt
@@ -26,18 +26,17 @@ import android.content.Intent.ACTION_VIEW
import android.net.Uri
import android.webkit.CookieManager
import androidx.compose.runtime.staticCompositionLocalOf
-import com.twidere.twiderex.db.model.ReferenceType
-import com.twidere.twiderex.model.MastodonStatusType
import com.twidere.twiderex.model.MicroBlogKey
-import com.twidere.twiderex.model.PlatformType
+import com.twidere.twiderex.model.enums.MastodonStatusType
+import com.twidere.twiderex.model.enums.PlatformType
+import com.twidere.twiderex.model.enums.ReferenceType
import com.twidere.twiderex.model.ui.UiStatus
import com.twidere.twiderex.model.ui.UiUser
-import com.twidere.twiderex.navigation.Route
+import com.twidere.twiderex.navigation.RootRoute
import com.twidere.twiderex.navigation.twidereXSchema
import com.twidere.twiderex.viewmodel.compose.ComposeType
import moe.tlaster.precompose.navigation.NavController
import moe.tlaster.precompose.navigation.NavOptions
-import moe.tlaster.precompose.navigation.PopUpTo
val LocalNavigator = staticCompositionLocalOf { error("No Navigator") }
@@ -61,7 +60,6 @@ interface INavigator {
fun openLink(it: String, deepLink: Boolean = true) {}
suspend fun twitterSignInWeb(target: String): String = ""
- suspend fun mastodonSignInWeb(target: String): String = ""
fun searchInput(initial: String? = null) {}
fun hashtag(name: String) {}
fun goBack() {}
@@ -72,7 +70,7 @@ class Navigator(
private val context: Context,
) : INavigator {
override fun user(user: UiUser, navOptions: NavOptions?) {
- navController.navigate(Route.User(user.userKey), navOptions)
+ navController.navigate(RootRoute.User(user.userKey), navOptions)
}
override fun status(status: UiStatus, navOptions: NavOptions?) {
@@ -94,7 +92,7 @@ class Navigator(
}
if (statusKey != null) {
navController.navigate(
- Route.Status(statusKey),
+ RootRoute.Status(statusKey),
navOptions
)
}
@@ -105,17 +103,16 @@ class Navigator(
selectedIndex: Int,
navOptions: NavOptions?
) {
- navController.navigate(Route.Media.Status(statusKey, selectedIndex), navOptions)
+ navController.navigate(RootRoute.Media.Status(statusKey, selectedIndex), navOptions)
}
override fun search(keyword: String) {
- navController.navigate(Route.Search(keyword), NavOptions(popUpTo = PopUpTo(Route.Home)))
+ navController.navigate(RootRoute.Search.Result(keyword))
}
override fun searchInput(initial: String?) {
navController.navigate(
- Route.SearchInput(initial),
- NavOptions(popUpTo = PopUpTo(Route.Home))
+ RootRoute.Search.Input(initial),
)
}
@@ -124,7 +121,7 @@ class Navigator(
statusKey: MicroBlogKey?,
navOptions: NavOptions?
) {
- navController.navigate(Route.Compose(composeType, statusKey))
+ navController.navigate(RootRoute.Compose.Home(composeType, statusKey))
}
override fun openLink(it: String, deepLink: Boolean) {
@@ -147,20 +144,12 @@ class Navigator(
CookieManager.getInstance().removeAllCookies {
}
return navController.navigateForResult(
- Route.SignIn.Web.Twitter(target)
- ).toString()
- }
-
- override suspend fun mastodonSignInWeb(target: String): String {
- CookieManager.getInstance().removeAllCookies {
- }
- return navController.navigateForResult(
- Route.SignIn.Web.Mastodon(target)
+ RootRoute.SignIn.Web.Twitter(target)
).toString()
}
override fun hashtag(name: String) {
- navController.navigate(Route.Mastodon.Hashtag(name))
+ navController.navigate(RootRoute.Mastodon.Hashtag(name))
}
override fun goBack() {
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/requireAuthorization.kt b/app/src/main/kotlin/com/twidere/twiderex/component/requireAuthorization.kt
index 0c629a310..6695efad1 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/component/requireAuthorization.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/requireAuthorization.kt
@@ -22,7 +22,7 @@ package com.twidere.twiderex.component
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
-import com.twidere.twiderex.navigation.Route
+import com.twidere.twiderex.navigation.RootRoute
import com.twidere.twiderex.ui.LocalActiveAccount
import com.twidere.twiderex.ui.LocalActivity
import com.twidere.twiderex.ui.LocalNavController
@@ -36,7 +36,7 @@ fun RequireAuthorization(
val navController = LocalNavController.current
val activity = LocalActivity.current
LaunchedEffect(Unit) {
- val result = navController.navigateForResult(Route.SignIn.Default)
+ val result = navController.navigateForResult(RootRoute.SignIn.General)
if (result == null) {
activity.finish()
}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/settings/SwitchItem.kt b/app/src/main/kotlin/com/twidere/twiderex/component/settings/SwitchItem.kt
index f5f6f64be..d78ab5add 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/component/settings/SwitchItem.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/settings/SwitchItem.kt
@@ -33,6 +33,7 @@ import com.twidere.twiderex.component.foundation.ColoredSwitch
fun ColumnScope.switchItem(
value: Boolean,
onChanged: (Boolean) -> Unit,
+ describe: @Composable (() -> Unit)? = null,
title: @Composable () -> Unit,
) {
ListItem(
@@ -47,6 +48,7 @@ fun ColumnScope.switchItem(
onChanged.invoke(it)
},
)
- }
+ },
+ secondaryText = describe
)
}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/status/DetailedStatusComponent.kt b/app/src/main/kotlin/com/twidere/twiderex/component/status/DetailedStatusComponent.kt
index ff5904146..7814d1a7d 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/component/status/DetailedStatusComponent.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/status/DetailedStatusComponent.kt
@@ -45,11 +45,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.twidere.twiderex.R
import com.twidere.twiderex.component.FormattedTime
-import com.twidere.twiderex.model.PlatformType
+import com.twidere.twiderex.extensions.humanizedCount
+import com.twidere.twiderex.model.enums.PlatformType
import com.twidere.twiderex.model.ui.UiStatus
@Composable
@@ -84,7 +84,7 @@ fun DetailedStatusComponent(
CompositionLocalProvider(
LocalContentAlpha provides ContentAlpha.disabled
) {
- if (!status.placeString.isNullOrEmpty()) {
+ if (status.geo.name.isNotEmpty()) {
Row(
modifier = Modifier
.align(Alignment.CenterHorizontally)
@@ -96,7 +96,7 @@ fun DetailedStatusComponent(
id = R.string.accessibility_common_status_location
)
)
- Text(text = status.placeString)
+ Text(text = status.geo.name)
}
Spacer(modifier = Modifier.height(DetailedStatusDefaults.ContentSpacing))
}
@@ -107,10 +107,12 @@ fun DetailedStatusComponent(
) {
FormattedTime(time = status.timestamp)
Spacer(modifier = Modifier.width(DetailedStatusDefaults.TimestampSpacing))
- Text(
- text = status.source,
+ HtmlText(
+ htmlText = status.source,
maxLines = 1,
- overflow = TextOverflow.Ellipsis,
+ linkResolver = {
+ ResolvedLink(null)
+ }
)
}
@@ -121,20 +123,20 @@ fun DetailedStatusComponent(
horizontalArrangement = Arrangement.Center,
) {
StatusStatistics(
- count = status.replyCount,
+ count = status.metrics.reply,
icon = painterResource(id = R.drawable.ic_corner_up_left),
contentDescription = stringResource(
id = R.string.scene_status_reply_mutiple,
- status.replyCount,
+ status.metrics.reply,
),
)
Spacer(modifier = Modifier.width(DetailedStatusDefaults.StatusStatisticsSpacing))
StatusStatistics(
- count = status.retweetCount,
+ count = status.metrics.retweet,
icon = painterResource(id = R.drawable.ic_repeat),
contentDescription = stringResource(
id = R.string.scene_status_retweet_mutiple,
- status.retweetCount,
+ status.metrics.retweet,
),
)
if (status.platformType == PlatformType.Twitter) {
@@ -147,11 +149,11 @@ fun DetailedStatusComponent(
}
Spacer(modifier = Modifier.width(DetailedStatusDefaults.StatusStatisticsSpacing))
StatusStatistics(
- count = status.likeCount,
+ count = status.metrics.like,
icon = painterResource(id = R.drawable.ic_heart),
contentDescription = stringResource(
id = R.string.scene_status_like_multiple,
- status.likeCount,
+ status.metrics.like,
),
)
}
@@ -204,7 +206,7 @@ private fun StatusStatistics(
contentDescription = contentDescription,
)
Spacer(modifier = Modifier.width(StatusStatisticsDefaults.IconSpacing))
- Text(text = count.toString())
+ Text(text = count.humanizedCount())
}
}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/status/HtmlText.kt b/app/src/main/kotlin/com/twidere/twiderex/component/status/HtmlText.kt
index 7fea533db..64d874de1 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/component/status/HtmlText.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/status/HtmlText.kt
@@ -31,22 +31,30 @@ import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ExperimentalComposeApi
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.consumeDownChange
import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.PlaceholderVerticalAlign
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
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.style.TextAlign
+import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.ExperimentalUnitApi
+import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.TextUnit
-import androidx.compose.ui.unit.TextUnitType
import com.twidere.twiderex.component.foundation.NetworkImage
import com.twidere.twiderex.component.navigation.LocalNavigator
import kotlinx.coroutines.coroutineScope
@@ -54,6 +62,7 @@ import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import org.jsoup.nodes.Node
import org.jsoup.nodes.TextNode
+import java.text.Bidi
private const val TAG_URL = "url"
@@ -72,22 +81,55 @@ fun HtmlText(
modifier: Modifier = Modifier,
htmlText: String,
maxLines: Int = Int.MAX_VALUE,
+ color: Color = Color.Unspecified,
+ fontSize: TextUnit = TextUnit.Unspecified,
+ fontStyle: FontStyle? = null,
+ fontWeight: FontWeight? = null,
+ fontFamily: FontFamily? = null,
+ letterSpacing: TextUnit = TextUnit.Unspecified,
+ textDecoration: TextDecoration? = null,
+ textAlign: TextAlign? = null,
+ lineHeight: TextUnit = TextUnit.Unspecified,
+ overflow: TextOverflow = TextOverflow.Ellipsis,
+ softWrap: Boolean = true,
textStyle: TextStyle = LocalTextStyle.current.copy(color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current)),
linkStyle: TextStyle = textStyle.copy(MaterialTheme.colors.primary),
linkResolver: (href: String) -> ResolvedLink = { ResolvedLink(it) },
) {
- val navigator = LocalNavigator.current
- RenderContent(
- modifier = modifier,
- htmlText = htmlText,
- linkResolver = linkResolver,
- maxLines = maxLines,
- textStyle = textStyle,
- linkStyle = linkStyle,
- onLinkClicked = {
- navigator.openLink(it)
- },
- )
+ val bidi = remember(htmlText) {
+ Bidi(htmlText, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT)
+ }
+ CompositionLocalProvider(
+ LocalLayoutDirection provides if (bidi.baseIsLeftToRight()) {
+ LayoutDirection.Ltr
+ } else {
+ LayoutDirection.Rtl
+ }
+ ) {
+ val navigator = LocalNavigator.current
+ RenderContent(
+ modifier = modifier,
+ htmlText = htmlText,
+ linkResolver = linkResolver,
+ maxLines = maxLines,
+ textStyle = textStyle,
+ linkStyle = linkStyle,
+ onLinkClicked = {
+ navigator.openLink(it)
+ },
+ color = color,
+ fontSize = fontSize,
+ fontStyle = fontStyle,
+ fontWeight = fontWeight,
+ fontFamily = fontFamily,
+ letterSpacing = letterSpacing,
+ textDecoration = textDecoration,
+ textAlign = textAlign,
+ lineHeight = lineHeight,
+ overflow = overflow,
+ softWrap = softWrap,
+ )
+ }
}
@OptIn(ExperimentalUnitApi::class)
@@ -95,11 +137,22 @@ fun HtmlText(
private fun RenderContent(
modifier: Modifier = Modifier,
htmlText: String,
- maxLines: Int = Int.MAX_VALUE,
textStyle: TextStyle,
linkStyle: TextStyle,
linkResolver: (href: String) -> ResolvedLink = { ResolvedLink(it) },
onLinkClicked: (String) -> Unit = {},
+ maxLines: Int = Int.MAX_VALUE,
+ color: Color = Color.Unspecified,
+ fontSize: TextUnit = TextUnit.Unspecified,
+ fontStyle: FontStyle? = null,
+ fontWeight: FontWeight? = null,
+ fontFamily: FontFamily? = null,
+ letterSpacing: TextUnit = TextUnit.Unspecified,
+ textDecoration: TextDecoration? = null,
+ textAlign: TextAlign? = null,
+ lineHeight: TextUnit = TextUnit.Unspecified,
+ overflow: TextOverflow = TextOverflow.Clip,
+ softWrap: Boolean = true,
) {
val value = renderContentAnnotatedString(
htmlText = htmlText,
@@ -133,8 +186,18 @@ private fun RenderContent(
}
}
},
- overflow = TextOverflow.Ellipsis,
maxLines = maxLines,
+ color = color,
+ fontSize = fontSize,
+ fontStyle = fontStyle,
+ fontWeight = fontWeight,
+ fontFamily = fontFamily,
+ letterSpacing = letterSpacing,
+ textDecoration = textDecoration,
+ textAlign = textAlign,
+ lineHeight = lineHeight,
+ overflow = overflow,
+ softWrap = softWrap,
text = value,
onTextLayout = {
layoutResult.value = it
@@ -161,7 +224,7 @@ private fun RenderContent(
@Composable
fun renderContentAnnotatedString(
htmlText: String,
- textStyle: TextStyle = MaterialTheme.typography.body1.copy(color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current), letterSpacing = TextUnit(0.25f, TextUnitType.Sp)),
+ textStyle: TextStyle = LocalTextStyle.current.copy(color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current)),
linkStyle: TextStyle = textStyle.copy(MaterialTheme.colors.primary),
linkResolver: (href: String) -> ResolvedLink,
): AnnotatedString {
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/status/MastodonPoll.kt b/app/src/main/kotlin/com/twidere/twiderex/component/status/MastodonPoll.kt
index a84c46076..715b00909 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/component/status/MastodonPoll.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/status/MastodonPoll.kt
@@ -66,39 +66,39 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
-import com.twidere.services.mastodon.model.Option
-import com.twidere.services.mastodon.model.Poll
import com.twidere.twiderex.R
import com.twidere.twiderex.action.LocalStatusActions
import com.twidere.twiderex.extensions.humanizedTimestamp
-import com.twidere.twiderex.model.PlatformType
+import com.twidere.twiderex.model.enums.PlatformType
+import com.twidere.twiderex.model.ui.Option
+import com.twidere.twiderex.model.ui.UiPoll
import com.twidere.twiderex.model.ui.UiStatus
import com.twidere.twiderex.ui.LocalActiveAccount
import kotlin.math.max
-private val Poll.canVote: Boolean
- get() = voted != true &&
- !(expired ?: false) &&
- expiresAt?.time?.let { it > System.currentTimeMillis() } ?: true // some instance allows expires time == null
+private val UiPoll.canVote: Boolean
+ get() = !voted &&
+ !expired &&
+ expiresAt?.let { it > System.currentTimeMillis() } ?: true // some instance allows expires time == null
@Composable
fun MastodonPoll(status: UiStatus) {
val account = LocalActiveAccount.current ?: return
- if (status.platformType != PlatformType.Mastodon || status.mastodonExtra?.poll == null) {
+ if (status.platformType != PlatformType.Mastodon || status.poll == null) {
return
}
val voteState = remember {
mutableStateListOf()
}
- status.mastodonExtra.poll.options?.forEachIndexed { index, option ->
+ status.poll.options.forEachIndexed { index, option ->
MastodonPollOption(
option,
index,
- status.mastodonExtra.poll,
+ status.poll,
voted = voteState.contains(index),
onVote = {
- if (status.mastodonExtra.poll.multiple == true) {
+ if (status.poll.multiple) {
if (voteState.contains(index)) {
voteState.remove(index)
} else {
@@ -114,7 +114,7 @@ fun MastodonPoll(status: UiStatus) {
}
}
)
- if (index != status.mastodonExtra.poll.options?.lastIndex) {
+ if (index != status.poll.options.lastIndex) {
Spacer(modifier = Modifier.height(MastodonPollDefaults.OptionSpacing))
}
}
@@ -125,7 +125,7 @@ fun MastodonPoll(status: UiStatus) {
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
- val countText = status.mastodonExtra.poll.votersCount?.let {
+ val countText = status.poll.votersCount?.let {
if (it > 1) {
stringResource(
id = R.string.common_controls_status_poll_total_people,
@@ -137,7 +137,7 @@ fun MastodonPoll(status: UiStatus) {
it,
)
}
- } ?: status.mastodonExtra.poll.votesCount?.let {
+ } ?: status.poll.votesCount?.let {
if (it > 1) {
stringResource(
id = R.string.common_controls_status_poll_total_votes,
@@ -161,15 +161,15 @@ fun MastodonPoll(status: UiStatus) {
Text(text = countText)
}
Spacer(modifier = Modifier.width(MastodonPollDefaults.VoteTimeSpacing))
- if (status.mastodonExtra.poll.expired == true) {
+ if (status.poll.expired) {
Text(text = stringResource(id = R.string.common_controls_status_poll_expired))
} else {
- Text(text = status.mastodonExtra.poll.expiresAt?.time?.humanizedTimestamp() ?: "")
+ Text(text = status.poll.expiresAt?.humanizedTimestamp() ?: "")
}
}
}
- if (status.mastodonExtra.poll.canVote) {
+ if (status.poll.canVote) {
val statusActions = LocalStatusActions.current
TextButton(
onClick = {
@@ -195,21 +195,21 @@ object MastodonPollDefaults {
fun MastodonPollOption(
option: Option,
index: Int,
- poll: Poll,
+ poll: UiPoll,
voted: Boolean,
onVote: (voted: Boolean) -> Unit = {},
) {
- val transition = updateTransition(targetState = option.votesCount)
+ val transition = updateTransition(targetState = option.count)
val progress by transition.animateFloat {
- (it ?: 0).toFloat() / max((poll.votesCount ?: 0), 1).toFloat()
+ it.toFloat() / max((poll.votesCount ?: 0), 1).toFloat()
}
- val color = if (poll.expired == true) {
+ val color = if (poll.expired) {
MaterialTheme.colors.onBackground
} else {
MaterialTheme.colors.primary
}
CompositionLocalProvider(
- *if (poll.expired == true) {
+ *if (poll.expired) {
arrayOf(LocalContentAlpha provides ContentAlpha.medium)
} else {
emptyArray()
@@ -327,7 +327,7 @@ fun MastodonPollOption(
Spacer(modifier = Modifier.width(MastodonPollOptionDefaults.IconSpacing))
Text(
modifier = Modifier.weight(1f),
- text = option.title ?: "",
+ text = option.text,
style = MaterialTheme.typography.body2
)
Spacer(modifier = Modifier.width(MastodonPollOptionDefaults.IconSpacing))
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/status/StatusActions.kt b/app/src/main/kotlin/com/twidere/twiderex/component/status/StatusActions.kt
index 1521214fd..752fb1c79 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/component/status/StatusActions.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/status/StatusActions.kt
@@ -64,7 +64,7 @@ import com.twidere.twiderex.action.LocalStatusActions
import com.twidere.twiderex.component.navigation.LocalNavigator
import com.twidere.twiderex.extensions.humanizedCount
import com.twidere.twiderex.extensions.shareText
-import com.twidere.twiderex.model.PlatformType
+import com.twidere.twiderex.model.enums.PlatformType
import com.twidere.twiderex.model.ui.UiStatus
import com.twidere.twiderex.ui.LocalActiveAccount
import com.twidere.twiderex.viewmodel.compose.ComposeType
@@ -89,7 +89,7 @@ fun ReplyButton(
modifier = modifier,
icon = icon,
color = LocalContentColor.current,
- count = data.replyCount,
+ count = data.metrics.reply,
contentDescription = contentDescription,
onClick = {
action.invoke()
@@ -136,7 +136,7 @@ fun LikeButton(
StatusActionButtonWithNumbers(
modifier = modifier,
icon = icon,
- count = data.likeCount,
+ count = data.metrics.like,
color = color,
contentDescription = contentDescription,
onClick = {
@@ -217,7 +217,7 @@ fun RetweetButton(
if (withNumber) {
StatusActionButtonWithNumbers(
icon = icon,
- count = data.retweetCount,
+ count = data.metrics.retweet,
color = color,
contentDescription = contentDescription,
onClick = {
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/status/StatusMediaComponent.kt b/app/src/main/kotlin/com/twidere/twiderex/component/status/StatusMediaComponent.kt
index 7f5e13dcd..f5213351a 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/component/status/StatusMediaComponent.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/status/StatusMediaComponent.kt
@@ -59,8 +59,8 @@ import com.twidere.twiderex.component.foundation.NetworkBlurImage
import com.twidere.twiderex.component.foundation.NetworkImage
import com.twidere.twiderex.component.foundation.VideoPlayer
import com.twidere.twiderex.component.navigation.LocalNavigator
-import com.twidere.twiderex.model.MediaType
-import com.twidere.twiderex.model.PlatformType
+import com.twidere.twiderex.model.enums.MediaType
+import com.twidere.twiderex.model.enums.PlatformType
import com.twidere.twiderex.model.ui.UiMedia
import com.twidere.twiderex.model.ui.UiStatus
import com.twidere.twiderex.ui.TwidereTheme
@@ -81,7 +81,7 @@ fun StatusMediaComponent(
navigator.media(statusKey = status.statusKey, selectedIndex = index)
}
var sensitive by rememberSaveable(status.statusKey.toString()) {
- mutableStateOf(status.mastodonExtra?.sensitive ?: false)
+ mutableStateOf(status.sensitive)
}
val aspectRatio = when (media.size) {
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/status/StatusText.kt b/app/src/main/kotlin/com/twidere/twiderex/component/status/StatusText.kt
index 08f3d0ee8..38b120880 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/component/status/StatusText.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/status/StatusText.kt
@@ -29,6 +29,7 @@ import androidx.compose.foundation.layout.ColumnScope
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.padding
import androidx.compose.material.Icon
@@ -45,7 +46,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.twidere.twiderex.R
-import com.twidere.twiderex.model.PlatformType
+import com.twidere.twiderex.model.enums.PlatformType
import com.twidere.twiderex.model.ui.UiStatus
@OptIn(ExperimentalAnimationApi::class)
@@ -56,12 +57,12 @@ fun ColumnScope.StatusText(
showMastodonPoll: Boolean = true
) {
val expandable = status.platformType == PlatformType.Mastodon &&
- status.mastodonExtra?.spoilerText != null
+ status.spoilerText != null
var expanded by rememberSaveable { mutableStateOf(!expandable) }
- if (expandable && status.mastodonExtra?.spoilerText != null) {
- Text(text = status.mastodonExtra.spoilerText)
+ if (expandable && status.spoilerText != null) {
+ Text(text = status.spoilerText)
Spacer(modifier = Modifier.height(StatusTextDefaults.Mastodon.SpoilerSpacing))
Row(
modifier = Modifier
@@ -85,14 +86,14 @@ fun ColumnScope.StatusText(
AnimatedVisibility(visible = expanded) {
Column {
HtmlText(
+ modifier = Modifier.fillMaxWidth(),
htmlText = status.htmlText,
maxLines = maxLines,
linkResolver = { href ->
status.resolveLink(href)
},
)
-
- if (showMastodonPoll && status.platformType == PlatformType.Mastodon && status.mastodonExtra?.poll != null) {
+ if (showMastodonPoll && status.platformType == PlatformType.Mastodon && status.poll != null) {
Spacer(modifier = Modifier.height(StatusTextDefaults.Mastodon.PollSpacing))
MastodonPoll(status)
}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/status/TimelineStatusComponent.kt b/app/src/main/kotlin/com/twidere/twiderex/component/status/TimelineStatusComponent.kt
index 770f78c18..581f35072 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/component/status/TimelineStatusComponent.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/status/TimelineStatusComponent.kt
@@ -68,12 +68,12 @@ import androidx.constraintlayout.compose.Dimension
import com.twidere.twiderex.R
import com.twidere.twiderex.component.HumanizedTime
import com.twidere.twiderex.component.navigation.LocalNavigator
-import com.twidere.twiderex.db.model.DbMastodonStatusExtra
-import com.twidere.twiderex.db.model.DbPreviewCard
import com.twidere.twiderex.extensions.icon
-import com.twidere.twiderex.model.MastodonStatusType
-import com.twidere.twiderex.model.PlatformType
+import com.twidere.twiderex.model.enums.MastodonStatusType
+import com.twidere.twiderex.model.enums.PlatformType
+import com.twidere.twiderex.model.ui.UiCard
import com.twidere.twiderex.model.ui.UiStatus
+import com.twidere.twiderex.model.ui.mastodon.MastodonStatusExtra
import com.twidere.twiderex.preferences.LocalDisplayPreferences
import com.twidere.twiderex.ui.LocalActiveAccount
@@ -240,7 +240,7 @@ private object StatusHeaderDefaults {
@Composable
private fun MastodonStatusHeader(
- mastodonExtra: DbMastodonStatusExtra,
+ mastodonExtra: MastodonStatusExtra,
data: UiStatus
) {
when (mastodonExtra.type) {
@@ -519,8 +519,13 @@ fun statusConstraintSets() = ConstraintSet {
val footerRef = createRefFor(StatusContentDefaults.Ref.Footer)
val lineUpRef = createRefFor(StatusContentDefaults.Ref.LineUp)
val lineDownRef = createRefFor(StatusContentDefaults.Ref.LineDown)
+ constrain(statusHeaderRef) {
+ top.linkTo(parent.top)
+ start.linkTo(parent.start)
+ }
constrain(avatarRef) {
top.linkTo(statusHeaderRef.bottom)
+ start.linkTo(parent.start)
}
constrain(contentRef) {
start.linkTo(avatarRef.end, margin = StatusContentDefaults.AvatarSpacing)
@@ -594,13 +599,13 @@ fun ColumnScope.StatusBody(
StatusBodyMedia(status)
if (LocalDisplayPreferences.current.urlPreview && !status.media.any()) {
- status.linkPreview?.let {
+ status.card?.let {
Spacer(modifier = Modifier.height(StatusBodyDefaults.LinkPreviewSpacing))
StatusLinkPreview(it)
}
}
- if (!status.placeString.isNullOrEmpty() && type == StatusContentType.Normal) {
+ if (status.geo.name.isNotEmpty() && type == StatusContentType.Normal) {
Spacer(modifier = Modifier.height(StatusBodyDefaults.PlaceSpacing))
CompositionLocalProvider(
LocalContentAlpha provides ContentAlpha.disabled
@@ -614,7 +619,7 @@ fun ColumnScope.StatusBody(
contentDescription = stringResource(id = R.string.accessibility_common_status_location)
)
Box(modifier = Modifier.width(StatusBodyDefaults.PlaceSpacing))
- Text(text = status.placeString)
+ Text(text = status.geo.name)
}
}
}
@@ -648,7 +653,7 @@ object StatusBodyDefaults {
}
@Composable
-private fun StatusLinkPreview(card: DbPreviewCard) {
+private fun StatusLinkPreview(card: UiCard) {
val navigator = LocalNavigator.current
LinkPreview(
modifier = Modifier
@@ -659,7 +664,7 @@ private fun StatusLinkPreview(card: DbPreviewCard) {
link = card.displayLink ?: card.link,
title = card.title?.trim(),
image = card.image,
- desc = card.desc?.trim(),
+ desc = card.description?.trim(),
maxLines = 5,
)
}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/status/UserAvatar.kt b/app/src/main/kotlin/com/twidere/twiderex/component/status/UserAvatar.kt
index c73a9a466..d4b3cbb14 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/component/status/UserAvatar.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/status/UserAvatar.kt
@@ -37,7 +37,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.twidere.twiderex.R
import com.twidere.twiderex.component.navigation.LocalNavigator
-import com.twidere.twiderex.model.PlatformType
+import com.twidere.twiderex.model.enums.PlatformType
import com.twidere.twiderex.model.ui.UiUser
import com.twidere.twiderex.preferences.LocalDisplayPreferences
import com.twidere.twiderex.preferences.proto.DisplayPreferences
diff --git a/app/src/main/kotlin/com/twidere/twiderex/component/status/UserScreenName.kt b/app/src/main/kotlin/com/twidere/twiderex/component/status/UserScreenName.kt
index 39c5ef35c..58ddfc8b7 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/component/status/UserScreenName.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/component/status/UserScreenName.kt
@@ -28,7 +28,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
@@ -38,10 +37,11 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.TextUnit
import com.twidere.twiderex.model.ui.UiUser
+import com.twidere.twiderex.ui.LocalActiveAccount
@Composable
fun UserScreenName(user: UiUser) {
- UserScreenName(name = user.displayScreenName)
+ UserScreenName(name = user.getDisplayScreenName(LocalActiveAccount.current?.accountKey?.host))
}
@Composable
@@ -73,7 +73,6 @@ fun UserName(
overflow: TextOverflow = TextOverflow.Ellipsis,
softWrap: Boolean = true,
maxLines: Int = 1,
- onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current
) {
UserName(
@@ -91,7 +90,6 @@ fun UserName(
overflow = overflow,
softWrap = softWrap,
maxLines = maxLines,
- onTextLayout = onTextLayout,
style = style,
)
}
@@ -112,11 +110,10 @@ fun UserName(
overflow: TextOverflow = TextOverflow.Ellipsis,
softWrap: Boolean = true,
maxLines: Int = 1,
- onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current
) {
- Text(
- text = userName,
+ HtmlText(
+ htmlText = userName,
modifier = modifier,
color = color,
fontSize = fontSize,
@@ -130,7 +127,6 @@ fun UserName(
overflow = overflow,
softWrap = softWrap,
maxLines = maxLines,
- onTextLayout = onTextLayout,
- style = style,
+ textStyle = style,
)
}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/db/CacheDatabase.kt b/app/src/main/kotlin/com/twidere/twiderex/db/CacheDatabase.kt
index 1232b94c1..71414330e 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/db/CacheDatabase.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/db/CacheDatabase.kt
@@ -76,7 +76,7 @@ import javax.inject.Singleton
DbDMConversation::class,
DbDMEvent::class
],
- version = 18,
+ version = 20,
)
@TypeConverters(
MicroBlogKeyConverter::class,
diff --git a/app/src/main/kotlin/com/twidere/twiderex/db/dao/DirectMessageConversationDao.kt b/app/src/main/kotlin/com/twidere/twiderex/db/dao/DirectMessageConversationDao.kt
index 2fd347400..129377068 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/db/dao/DirectMessageConversationDao.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/db/dao/DirectMessageConversationDao.kt
@@ -20,7 +20,6 @@
*/
package com.twidere.twiderex.db.dao
-import androidx.lifecycle.LiveData
import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Delete
@@ -31,6 +30,7 @@ import androidx.room.Transaction
import com.twidere.twiderex.db.model.DbDMConversation
import com.twidere.twiderex.db.model.DbDirectMessageConversationWithMessage
import com.twidere.twiderex.model.MicroBlogKey
+import kotlinx.coroutines.flow.Flow
import org.jetbrains.annotations.TestOnly
@Dao
@@ -65,7 +65,7 @@ interface DirectMessageConversationDao {
): PagingSource
@Query("SELECT * FROM dm_conversation WHERE accountKey == :accountKey AND conversationKey == :conversationKey")
- fun findWithConversationKeyLiveData(accountKey: MicroBlogKey, conversationKey: MicroBlogKey): LiveData
+ fun findWithConversationKeyFlow(accountKey: MicroBlogKey, conversationKey: MicroBlogKey): Flow
@Query("SELECT * FROM dm_conversation WHERE accountKey == :accountKey AND conversationKey == :conversationKey")
fun findWithConversationKey(accountKey: MicroBlogKey, conversationKey: MicroBlogKey): DbDMConversation?
diff --git a/app/src/main/kotlin/com/twidere/twiderex/db/dao/DraftDao.kt b/app/src/main/kotlin/com/twidere/twiderex/db/dao/DraftDao.kt
index f633061f5..de21b3351 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/db/dao/DraftDao.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/db/dao/DraftDao.kt
@@ -20,13 +20,13 @@
*/
package com.twidere.twiderex.db.dao
-import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.twidere.twiderex.db.model.DbDraft
+import kotlinx.coroutines.flow.Flow
@Dao
interface DraftDao {
@@ -34,7 +34,7 @@ interface DraftDao {
suspend fun insertAll(vararg draft: DbDraft)
@Query("SELECT * FROM draft")
- fun getAll(): LiveData>
+ fun getAll(): Flow>
@Query("SELECT * FROM draft WHERE _id == :id")
suspend fun get(id: String): DbDraft?
@@ -43,5 +43,5 @@ interface DraftDao {
suspend fun remove(draft: DbDraft)
@Query("SELECT COUNT(*) FROM draft")
- fun getDraftCount(): LiveData
+ fun getDraftCount(): Flow
}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/db/dao/ListsDao.kt b/app/src/main/kotlin/com/twidere/twiderex/db/dao/ListsDao.kt
index 625ee37fe..b8174d8b9 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/db/dao/ListsDao.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/db/dao/ListsDao.kt
@@ -20,7 +20,6 @@
*/
package com.twidere.twiderex.db.dao
-import androidx.lifecycle.LiveData
import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Delete
@@ -31,6 +30,7 @@ import androidx.room.Transaction
import androidx.room.Update
import com.twidere.twiderex.db.model.DbList
import com.twidere.twiderex.model.MicroBlogKey
+import kotlinx.coroutines.flow.Flow
@Dao
interface ListsDao {
@@ -41,7 +41,7 @@ interface ListsDao {
suspend fun findWithListKey(listKey: MicroBlogKey, accountKey: MicroBlogKey): DbList?
@Query("SELECT * FROM lists WHERE listKey == :listKey AND accountKey == :accountKey")
- fun findWithListKeyWithLiveData(listKey: MicroBlogKey, accountKey: MicroBlogKey): LiveData
+ fun findWithListKeyWithFlow(listKey: MicroBlogKey, accountKey: MicroBlogKey): Flow
@Query("SELECT * FROM lists")
suspend fun findAll(): List?
diff --git a/app/src/main/kotlin/com/twidere/twiderex/db/dao/SearchDao.kt b/app/src/main/kotlin/com/twidere/twiderex/db/dao/SearchDao.kt
index cd8606bdf..6cc3edcb7 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/db/dao/SearchDao.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/db/dao/SearchDao.kt
@@ -20,7 +20,6 @@
*/
package com.twidere.twiderex.db.dao
-import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
@@ -28,6 +27,7 @@ import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.twidere.twiderex.db.model.DbSearch
import com.twidere.twiderex.model.MicroBlogKey
+import kotlinx.coroutines.flow.Flow
@Dao
interface SearchDao {
@@ -35,13 +35,13 @@ interface SearchDao {
suspend fun insertAll(search: List)
@Query("SELECT * FROM search where accountKey == :accountKey ORDER BY lastActive DESC")
- fun getAll(accountKey: MicroBlogKey): LiveData>
+ fun getAll(accountKey: MicroBlogKey): Flow>
@Query("SELECT * FROM search where saved == 0 AND accountKey == :accountKey ORDER BY lastActive DESC")
- fun getAllHistory(accountKey: MicroBlogKey): LiveData>
+ fun getAllHistory(accountKey: MicroBlogKey): Flow>
@Query("SELECT * FROM search where saved == 1 AND accountKey == :accountKey ORDER BY lastActive DESC")
- fun getAllSaved(accountKey: MicroBlogKey): LiveData>
+ fun getAllSaved(accountKey: MicroBlogKey): Flow>
@Query("SELECT * FROM search WHERE content == :content AND accountKey == :accountKey")
suspend fun get(content: String, accountKey: MicroBlogKey): DbSearch?
diff --git a/app/src/main/kotlin/com/twidere/twiderex/db/dao/StatusDao.kt b/app/src/main/kotlin/com/twidere/twiderex/db/dao/StatusDao.kt
index 83dd9b4eb..866cb2685 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/db/dao/StatusDao.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/db/dao/StatusDao.kt
@@ -20,7 +20,6 @@
*/
package com.twidere.twiderex.db.dao
-import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
@@ -31,6 +30,7 @@ import androidx.room.Update
import com.twidere.twiderex.db.model.DbStatusV2
import com.twidere.twiderex.db.model.DbStatusWithReference
import com.twidere.twiderex.model.MicroBlogKey
+import kotlinx.coroutines.flow.Flow
@Dao
interface StatusDao {
@@ -49,7 +49,7 @@ interface StatusDao {
@Transaction
@Query("SELECT * FROM status WHERE statusKey == :key")
- fun findWithStatusKeyWithReferenceLiveData(key: MicroBlogKey): LiveData
+ fun findWithStatusKeyWithReferenceFlow(key: MicroBlogKey): Flow
@Update(onConflict = OnConflictStrategy.REPLACE)
suspend fun update(status: List)
diff --git a/app/src/main/kotlin/com/twidere/twiderex/db/dao/StatusReferenceDao.kt b/app/src/main/kotlin/com/twidere/twiderex/db/dao/StatusReferenceDao.kt
index c7f18570e..fdb793754 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/db/dao/StatusReferenceDao.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/db/dao/StatusReferenceDao.kt
@@ -27,8 +27,8 @@ import androidx.room.Query
import androidx.room.Transaction
import com.twidere.twiderex.db.model.DbStatusReference
import com.twidere.twiderex.db.model.DbStatusReferenceWithStatus
-import com.twidere.twiderex.db.model.ReferenceType
import com.twidere.twiderex.model.MicroBlogKey
+import com.twidere.twiderex.model.enums.ReferenceType
@Dao
interface StatusReferenceDao {
diff --git a/app/src/main/kotlin/com/twidere/twiderex/db/dao/UserDao.kt b/app/src/main/kotlin/com/twidere/twiderex/db/dao/UserDao.kt
index 8d68a768c..252452254 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/db/dao/UserDao.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/db/dao/UserDao.kt
@@ -20,7 +20,6 @@
*/
package com.twidere.twiderex.db.dao
-import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
@@ -28,6 +27,7 @@ import androidx.room.Query
import androidx.room.Update
import com.twidere.twiderex.db.model.DbUser
import com.twidere.twiderex.model.MicroBlogKey
+import kotlinx.coroutines.flow.Flow
@Dao
interface UserDao {
@@ -35,7 +35,7 @@ interface UserDao {
suspend fun insertAll(user: List)
@Query("SELECT * FROM user WHERE userKey == :userKey")
- fun findWithUserKeyLiveData(userKey: MicroBlogKey): LiveData
+ fun findWithUserKeyFlow(userKey: MicroBlogKey): Flow
@Query("SELECT * FROM user WHERE userKey == :userKey")
suspend fun findWithUserKey(userKey: MicroBlogKey): DbUser?
diff --git a/app/src/main/kotlin/com/twidere/twiderex/db/mapper/Mastodon.kt b/app/src/main/kotlin/com/twidere/twiderex/db/mapper/Mastodon.kt
index 27586603a..0ee6fe2a7 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/db/mapper/Mastodon.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/db/mapper/Mastodon.kt
@@ -44,13 +44,15 @@ import com.twidere.twiderex.db.model.DbTrend
import com.twidere.twiderex.db.model.DbTrendHistory
import com.twidere.twiderex.db.model.DbTrendWithHistory
import com.twidere.twiderex.db.model.DbUser
-import com.twidere.twiderex.db.model.ReferenceType
import com.twidere.twiderex.db.model.toDbStatusReference
-import com.twidere.twiderex.model.MastodonStatusType
-import com.twidere.twiderex.model.MediaType
import com.twidere.twiderex.model.MicroBlogKey
-import com.twidere.twiderex.model.PlatformType
-import com.twidere.twiderex.navigation.DeepLinks
+import com.twidere.twiderex.model.enums.MastodonStatusType
+import com.twidere.twiderex.model.enums.MastodonVisibility
+import com.twidere.twiderex.model.enums.MediaType
+import com.twidere.twiderex.model.enums.PlatformType
+import com.twidere.twiderex.model.enums.ReferenceType
+import com.twidere.twiderex.navigation.RootDeepLinksRoute
+import com.twidere.twiderex.utils.json
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import org.jsoup.nodes.Node
@@ -103,16 +105,16 @@ fun Notification.toDbStatusWithReference(
lang = null,
is_possibly_sensitive = false,
platformType = PlatformType.Mastodon,
- mastodonExtra = DbMastodonStatusExtra(
+ extra = DbMastodonStatusExtra(
type = this.type.toDbType(),
emoji = emptyList(),
- visibility = Visibility.Public,
+ visibility = MastodonVisibility.Public,
sensitive = false,
spoilerText = null,
poll = null,
card = null,
mentions = null,
- ),
+ ).json(),
inReplyToStatusId = null,
inReplyToUserId = null,
)
@@ -190,7 +192,7 @@ private fun Status.toDbStatusWithMediaAndUser(
val status = DbStatusV2(
_id = UUID.randomUUID().toString(),
statusId = id ?: throw IllegalArgumentException("mastodon Status.idStr should not be null"),
- rawText = content ?: "",
+ rawText = content?.let { Jsoup.parse(it).wholeText() } ?: "",
htmlText = content?.let {
generateHtmlContentWithEmoji(
content = it,
@@ -220,16 +222,16 @@ private fun Status.toDbStatusWithMediaAndUser(
),
is_possibly_sensitive = sensitive ?: false,
platformType = PlatformType.Mastodon,
- mastodonExtra = DbMastodonStatusExtra(
+ extra = DbMastodonStatusExtra(
type = MastodonStatusType.Status,
emoji = emojis ?: emptyList(),
- visibility = visibility ?: Visibility.Public,
+ visibility = visibility.toDbEnums(),
sensitive = sensitive ?: false,
spoilerText = spoilerText?.takeIf { it.isNotEmpty() },
poll = poll,
card = card,
mentions = mentions,
- ),
+ ).json(),
previewCard = card?.url?.let { url ->
DbPreviewCard(
link = url,
@@ -291,8 +293,9 @@ fun Account.toDbUser(
return DbUser(
_id = UUID.randomUUID().toString(),
userId = this.id ?: throw IllegalArgumentException("mastodon user.id should not be null"),
- name = displayName
- ?: throw IllegalArgumentException("mastodon user.displayName should not be null"),
+ name = displayName?.let {
+ generateHtmlContentWithEmoji(it, emojis ?: emptyList())
+ } ?: throw IllegalArgumentException("mastodon user.displayName should not be null"),
screenName = username
?: throw IllegalArgumentException("mastodon user.username should not be null"),
userKey = MicroBlogKey(
@@ -326,12 +329,12 @@ fun Account.toDbUser(
} ?: throw IllegalArgumentException("mastodon user.acct should not be null"),
platformType = PlatformType.Mastodon,
statusesCount = statusesCount ?: 0L,
- mastodonExtra = DbMastodonUserExtra(
+ extra = DbMastodonUserExtra(
fields = fields ?: emptyList(),
bot = bot ?: false,
locked = locked ?: false,
emoji = emojis ?: emptyList(),
- )
+ ).json()
)
}
@@ -411,7 +414,7 @@ private fun replaceMention(mentions: List, node: Node, accountKey: Micr
if (id != null) {
node.attr(
"href",
- DeepLinks.User + "/" + MicroBlogKey(id, accountKey.host)
+ RootDeepLinksRoute.User(MicroBlogKey(id, accountKey.host))
)
}
} else {
@@ -431,9 +434,16 @@ private fun replaceHashTag(node: Node) {
) {
node.attr(
"href",
- DeepLinks.Mastodon.Hashtag + "/" + node.text().trimStart('#')
+ RootDeepLinksRoute.Mastodon.Hashtag(node.text().trimStart('#'))
)
} else {
node.childNodes().forEach { replaceHashTag(it) }
}
}
+
+private fun Visibility?.toDbEnums() = when (this) {
+ Visibility.Unlisted -> MastodonVisibility.Unlisted
+ Visibility.Private -> MastodonVisibility.Private
+ Visibility.Direct -> MastodonVisibility.Direct
+ Visibility.Public, null -> MastodonVisibility.Public
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/db/mapper/Twitter.kt b/app/src/main/kotlin/com/twidere/twiderex/db/mapper/Twitter.kt
index 0f501e63e..8e1056e1a 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/db/mapper/Twitter.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/db/mapper/Twitter.kt
@@ -46,26 +46,35 @@ import com.twidere.twiderex.db.model.DbTwitterStatusExtra
import com.twidere.twiderex.db.model.DbTwitterUserExtra
import com.twidere.twiderex.db.model.DbUrlEntity
import com.twidere.twiderex.db.model.DbUser
-import com.twidere.twiderex.db.model.ReferenceType
import com.twidere.twiderex.db.model.TwitterUrlEntity
import com.twidere.twiderex.db.model.toDbStatusReference
-import com.twidere.twiderex.model.MediaType
import com.twidere.twiderex.model.MicroBlogKey
-import com.twidere.twiderex.model.PlatformType
+import com.twidere.twiderex.model.enums.MediaType
+import com.twidere.twiderex.model.enums.PlatformType
+import com.twidere.twiderex.model.enums.ReferenceType
+import com.twidere.twiderex.model.enums.TwitterReplySettings
import com.twidere.twiderex.model.ui.ListsMode
-import com.twidere.twiderex.navigation.DeepLinks
+import com.twidere.twiderex.navigation.RootDeepLinksRouteDefinition
+import com.twidere.twiderex.utils.json
import com.twitter.twittertext.Autolink
import java.util.UUID
val autolink by lazy {
Autolink().apply {
setUsernameIncludeSymbol(true)
- hashtagUrlBase = "${DeepLinks.Search}/%23"
- cashtagUrlBase = "${DeepLinks.Search}/%24"
- usernameUrlBase = "${DeepLinks.Twitter.User}/"
+ hashtagUrlBase = "${generateDeepLinkBase(RootDeepLinksRouteDefinition.Search)}/%23"
+ cashtagUrlBase = "${generateDeepLinkBase(RootDeepLinksRouteDefinition.Search)}/%24"
+ usernameUrlBase = "${generateDeepLinkBase(RootDeepLinksRouteDefinition.Twitter.User)}/"
}
}
+private fun generateDeepLinkBase(deeplink: String): String {
+ return deeplink.substring(
+ 0,
+ deeplink.indexOf("/{")
+ )
+}
+
fun StatusV2.toDbPagingTimeline(
accountKey: MicroBlogKey,
pagingKey: String,
@@ -218,11 +227,10 @@ private fun StatusV2.toDbStatusWithMediaAndUser(
id ?: throw IllegalArgumentException("Status.idStr should not be null")
),
platformType = PlatformType.Twitter,
- mastodonExtra = null,
- twitterExtra = DbTwitterStatusExtra(
- reply_settings = replySettings ?: ReplySettings.Everyone,
+ extra = DbTwitterStatusExtra(
+ reply_settings = replySettings.toDbEnums(),
quoteCount = publicMetrics?.quoteCount
- ),
+ ).json(),
previewCard = entities?.urls?.firstOrNull()
?.takeUnless { url ->
referencedTweets?.firstOrNull { it.type == ReferencedTweetType.quoted }
@@ -330,10 +338,9 @@ private fun Status.toDbStatusWithMediaAndUser(
idStr ?: throw IllegalArgumentException("Status.idStr should not be null")
),
platformType = PlatformType.Twitter,
- mastodonExtra = null,
- twitterExtra = DbTwitterStatusExtra(
- reply_settings = ReplySettings.Everyone,
- ),
+ extra = DbTwitterStatusExtra(
+ reply_settings = TwitterReplySettings.Everyone,
+ ).json(),
previewCard = entities?.urls?.firstOrNull()
?.takeUnless { url -> quotedStatus?.idStr?.let { id -> url.expandedURL?.endsWith(id) == true } == true }
?.takeUnless { url -> url.expandedURL?.contains("pic.twitter.com") == true }
@@ -432,7 +439,7 @@ fun User.toDbUser(): DbUser {
platformType = PlatformType.Twitter,
acct = MicroBlogKey.twitter(screenName ?: ""),
statusesCount = statusesCount ?: 0,
- twitterExtra = DbTwitterUserExtra(
+ extra = DbTwitterUserExtra(
pinned_tweet_id = null,
url = entities?.description?.urls?.map {
TwitterUrlEntity(
@@ -441,7 +448,7 @@ fun User.toDbUser(): DbUser {
displayUrl = it.displayURL ?: "",
)
} ?: emptyList()
- )
+ ).json()
)
}
@@ -470,7 +477,7 @@ fun UserV2.toDbUser(): DbUser {
acct = MicroBlogKey.twitter(username ?: ""),
platformType = PlatformType.Twitter,
statusesCount = publicMetrics?.tweetCount ?: 0,
- twitterExtra = DbTwitterUserExtra(
+ extra = DbTwitterUserExtra(
pinned_tweet_id = pinnedTweetID,
url = entities?.description?.urls?.map {
TwitterUrlEntity(
@@ -479,7 +486,7 @@ fun UserV2.toDbUser(): DbUser {
displayUrl = it.displayURL ?: "",
)
} ?: emptyList()
- )
+ ).json()
)
}
@@ -600,3 +607,9 @@ fun DirectMessageEvent.toDbDirectMessage(accountKey: MicroBlogKey, sender: DbUse
sender = sender
)
}
+
+private fun ReplySettings?.toDbEnums() = when (this) {
+ ReplySettings.MentionedUsers -> TwitterReplySettings.MentionedUsers
+ ReplySettings.FollowingUsers -> TwitterReplySettings.FollowingUsers
+ ReplySettings.Everyone, null -> TwitterReplySettings.Everyone
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/db/model/Alias.kt b/app/src/main/kotlin/com/twidere/twiderex/db/model/Alias.kt
new file mode 100644
index 000000000..4ea627f62
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/db/model/Alias.kt
@@ -0,0 +1,25 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.db.model
+
+typealias Html = String
+typealias Json = String
+typealias Timestamp = Long
diff --git a/app/src/main/kotlin/com/twidere/twiderex/db/model/DbMedia.kt b/app/src/main/kotlin/com/twidere/twiderex/db/model/DbMedia.kt
index 7bcdaca7b..e6b857d73 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/db/model/DbMedia.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/db/model/DbMedia.kt
@@ -23,8 +23,8 @@ package com.twidere.twiderex.db.model
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
-import com.twidere.twiderex.model.MediaType
import com.twidere.twiderex.model.MicroBlogKey
+import com.twidere.twiderex.model.enums.MediaType
@Entity(
tableName = "media",
diff --git a/app/src/main/kotlin/com/twidere/twiderex/db/model/DbStatus.kt b/app/src/main/kotlin/com/twidere/twiderex/db/model/DbStatus.kt
index 9fc015789..51eb7f346 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/db/model/DbStatus.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/db/model/DbStatus.kt
@@ -30,12 +30,12 @@ import com.twidere.services.mastodon.model.Card
import com.twidere.services.mastodon.model.Emoji
import com.twidere.services.mastodon.model.Mention
import com.twidere.services.mastodon.model.Poll
-import com.twidere.services.mastodon.model.Visibility
-import com.twidere.services.twitter.model.ReplySettings
import com.twidere.twiderex.db.CacheDatabase
-import com.twidere.twiderex.model.MastodonStatusType
import com.twidere.twiderex.model.MicroBlogKey
-import com.twidere.twiderex.model.PlatformType
+import com.twidere.twiderex.model.enums.MastodonStatusType
+import com.twidere.twiderex.model.enums.MastodonVisibility
+import com.twidere.twiderex.model.enums.PlatformType
+import com.twidere.twiderex.model.enums.TwitterReplySettings
import kotlinx.serialization.Serializable
@Entity(
@@ -66,13 +66,14 @@ data class DbStatusV2(
val lang: String?,
val is_possibly_sensitive: Boolean,
val platformType: PlatformType,
- var mastodonExtra: DbMastodonStatusExtra? = null,
- val twitterExtra: DbTwitterStatusExtra? = null,
val previewCard: DbPreviewCard? = null,
val inReplyToUserId: String? = null,
- val inReplyToStatusId: String? = null
+ val inReplyToStatusId: String? = null,
+ var extra: Json
)
+interface DbStatusExtra
+
@Immutable
@Serializable
data class DbPreviewCard(
@@ -86,22 +87,22 @@ data class DbPreviewCard(
@Immutable
@Serializable
data class DbTwitterStatusExtra(
- val reply_settings: ReplySettings,
+ val reply_settings: TwitterReplySettings,
val quoteCount: Long? = null,
-)
+) : DbStatusExtra
@Immutable
@Serializable
data class DbMastodonStatusExtra(
val type: MastodonStatusType,
val emoji: List,
- val visibility: Visibility,
+ val visibility: MastodonVisibility,
val sensitive: Boolean,
val spoilerText: String?,
val poll: Poll?,
val card: Card?,
val mentions: List?,
-)
+) : DbStatusExtra
data class DbStatusWithMediaAndUser(
@Embedded
diff --git a/app/src/main/kotlin/com/twidere/twiderex/db/model/DbStatusReference.kt b/app/src/main/kotlin/com/twidere/twiderex/db/model/DbStatusReference.kt
index e25333e68..818ba7433 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/db/model/DbStatusReference.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/db/model/DbStatusReference.kt
@@ -27,6 +27,7 @@ import androidx.room.PrimaryKey
import androidx.room.Relation
import com.twidere.twiderex.db.CacheDatabase
import com.twidere.twiderex.model.MicroBlogKey
+import com.twidere.twiderex.model.enums.ReferenceType
import java.util.UUID
@Entity(
@@ -83,13 +84,6 @@ fun DbStatusWithMediaAndUser?.toDbStatusReference(
}
}
-enum class ReferenceType {
- Retweet,
- Reply,
- Quote,
- MastodonNotification,
-}
-
data class DbStatusWithReference(
@Embedded
val status: DbStatusWithMediaAndUser,
diff --git a/app/src/main/kotlin/com/twidere/twiderex/db/model/DbUser.kt b/app/src/main/kotlin/com/twidere/twiderex/db/model/DbUser.kt
index 513079244..a2a3eaed6 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/db/model/DbUser.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/db/model/DbUser.kt
@@ -25,7 +25,7 @@ import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import com.twidere.twiderex.model.MicroBlogKey
-import com.twidere.twiderex.model.PlatformType
+import com.twidere.twiderex.model.enums.PlatformType
import kotlinx.serialization.Serializable
@Entity(
@@ -56,8 +56,7 @@ data class DbUser(
val isProtected: Boolean,
val platformType: PlatformType,
val statusesCount: Long,
- val twitterExtra: DbTwitterUserExtra? = null,
- val mastodonExtra: DbMastodonUserExtra? = null,
+ val extra: Json
)
@Immutable
diff --git a/app/src/main/kotlin/com/twidere/twiderex/db/model/converter/MediaTypeConverter.kt b/app/src/main/kotlin/com/twidere/twiderex/db/model/converter/MediaTypeConverter.kt
index 1518288cd..fbb10fd21 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/db/model/converter/MediaTypeConverter.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/db/model/converter/MediaTypeConverter.kt
@@ -21,7 +21,7 @@
package com.twidere.twiderex.db.model.converter
import androidx.room.TypeConverter
-import com.twidere.twiderex.model.MediaType
+import com.twidere.twiderex.model.enums.MediaType
class MediaTypeConverter {
@TypeConverter
diff --git a/app/src/main/kotlin/com/twidere/twiderex/db/model/converter/NotificationTypeConverter.kt b/app/src/main/kotlin/com/twidere/twiderex/db/model/converter/NotificationTypeConverter.kt
index bbcca4a38..8e018b098 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/db/model/converter/NotificationTypeConverter.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/db/model/converter/NotificationTypeConverter.kt
@@ -21,7 +21,7 @@
package com.twidere.twiderex.db.model.converter
import androidx.room.TypeConverter
-import com.twidere.twiderex.model.MastodonStatusType
+import com.twidere.twiderex.model.enums.MastodonStatusType
class NotificationTypeConverter {
@TypeConverter
diff --git a/app/src/main/kotlin/com/twidere/twiderex/db/model/converter/PlatformTypeConverter.kt b/app/src/main/kotlin/com/twidere/twiderex/db/model/converter/PlatformTypeConverter.kt
index e4eb54d2d..77fd7cd6f 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/db/model/converter/PlatformTypeConverter.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/db/model/converter/PlatformTypeConverter.kt
@@ -21,7 +21,7 @@
package com.twidere.twiderex.db.model.converter
import androidx.room.TypeConverter
-import com.twidere.twiderex.model.PlatformType
+import com.twidere.twiderex.model.enums.PlatformType
class PlatformTypeConverter {
@TypeConverter
diff --git a/app/src/main/kotlin/com/twidere/twiderex/di/InitializerEntryPoint.kt b/app/src/main/kotlin/com/twidere/twiderex/di/InitializerEntryPoint.kt
index 7c17f7e71..af2b2e404 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/di/InitializerEntryPoint.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/di/InitializerEntryPoint.kt
@@ -21,6 +21,7 @@
package com.twidere.twiderex.di
import android.content.Context
+import com.twidere.twiderex.http.TwidereServiceInitializer
import com.twidere.twiderex.notification.NotificationChannelInitializer
import com.twidere.twiderex.notification.NotificationInitializer
import com.twidere.twiderex.worker.dm.DirectMessageInitializer
@@ -45,4 +46,5 @@ interface InitializerEntryPoint {
fun inject(initializer: NotificationChannelInitializer)
fun inject(initializer: NotificationInitializer)
fun inject(initializer: DirectMessageInitializer)
+ fun inject(initializer: TwidereServiceInitializer)
}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/di/JobModule.kt b/app/src/main/kotlin/com/twidere/twiderex/di/JobModule.kt
new file mode 100644
index 000000000..86883f273
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/di/JobModule.kt
@@ -0,0 +1,260 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.di
+
+import android.content.Context
+import com.twidere.twiderex.db.CacheDatabase
+import com.twidere.twiderex.jobs.common.DownloadMediaJob
+import com.twidere.twiderex.jobs.common.NotificationJob
+import com.twidere.twiderex.jobs.common.ShareMediaJob
+import com.twidere.twiderex.jobs.compose.MastodonComposeJob
+import com.twidere.twiderex.jobs.compose.TwitterComposeJob
+import com.twidere.twiderex.jobs.database.DeleteDbStatusJob
+import com.twidere.twiderex.jobs.dm.DirectMessageDeleteJob
+import com.twidere.twiderex.jobs.dm.DirectMessageFetchJob
+import com.twidere.twiderex.jobs.dm.TwitterDirectMessageSendJob
+import com.twidere.twiderex.jobs.draft.RemoveDraftJob
+import com.twidere.twiderex.jobs.draft.SaveDraftJob
+import com.twidere.twiderex.jobs.status.DeleteStatusJob
+import com.twidere.twiderex.jobs.status.LikeStatusJob
+import com.twidere.twiderex.jobs.status.MastodonVoteJob
+import com.twidere.twiderex.jobs.status.RetweetStatusJob
+import com.twidere.twiderex.jobs.status.UnRetweetStatusJob
+import com.twidere.twiderex.jobs.status.UnlikeStatusJob
+import com.twidere.twiderex.kmp.ExifScrambler
+import com.twidere.twiderex.kmp.FileResolver
+import com.twidere.twiderex.kmp.RemoteNavigator
+import com.twidere.twiderex.notification.AppNotificationManager
+import com.twidere.twiderex.notification.InAppNotification
+import com.twidere.twiderex.repository.AccountRepository
+import com.twidere.twiderex.repository.DirectMessageRepository
+import com.twidere.twiderex.repository.DraftRepository
+import com.twidere.twiderex.repository.NotificationRepository
+import com.twidere.twiderex.repository.StatusRepository
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+
+@Module
+@InstallIn(SingletonComponent::class)
+object JobModule {
+
+ @Provides
+ fun provideShareMediaJob(
+ fileResolver: FileResolver,
+ remoteNavigator: RemoteNavigator
+ ): ShareMediaJob = ShareMediaJob(
+ fileResolver = fileResolver,
+ remoteNavigator = remoteNavigator
+ )
+
+ @Provides
+ fun provideDownloadMediaJob(
+ accountRepository: AccountRepository,
+ inAppNotification: InAppNotification,
+ fileResolver: FileResolver,
+ ): DownloadMediaJob = DownloadMediaJob(
+ accountRepository = accountRepository,
+ inAppNotification = inAppNotification,
+ fileResolver = fileResolver
+ )
+
+ @Provides
+ fun provideDeleteDbStatusJob(
+ statusRepository: StatusRepository
+ ): DeleteDbStatusJob = DeleteDbStatusJob(
+ statusRepository = statusRepository
+ )
+
+ @Provides
+ fun provideDeleteStatusJob(
+ accountRepository: AccountRepository,
+ inAppNotification: InAppNotification,
+ statusRepository: StatusRepository
+ ): DeleteStatusJob = DeleteStatusJob(
+ accountRepository = accountRepository,
+ statusRepository = statusRepository,
+ inAppNotification = inAppNotification
+ )
+
+ @Provides
+ fun provideNotificationJob(
+ @ApplicationContext context: Context,
+ accountRepository: AccountRepository,
+ repository: NotificationRepository,
+ notificationManager: AppNotificationManager
+ ): NotificationJob = NotificationJob(
+ applicationContext = context,
+ repository = repository,
+ accountRepository = accountRepository,
+ notificationManager = notificationManager
+ )
+
+ @Provides
+ fun provideLikeStatusJob(
+ accountRepository: AccountRepository,
+ statusRepository: StatusRepository,
+ inAppNotification: InAppNotification
+ ): LikeStatusJob = LikeStatusJob(
+ accountRepository = accountRepository,
+ statusRepository = statusRepository,
+ inAppNotification = inAppNotification
+ )
+
+ @Provides
+ fun provideRetweetStatusJob(
+ accountRepository: AccountRepository,
+ statusRepository: StatusRepository,
+ inAppNotification: InAppNotification
+ ): RetweetStatusJob = RetweetStatusJob(
+ accountRepository = accountRepository,
+ statusRepository = statusRepository,
+ inAppNotification = inAppNotification
+ )
+
+ @Provides
+ fun provideUnlikeStatusJob(
+ accountRepository: AccountRepository,
+ statusRepository: StatusRepository,
+ inAppNotification: InAppNotification
+ ): UnlikeStatusJob = UnlikeStatusJob(
+ accountRepository = accountRepository,
+ statusRepository = statusRepository,
+ inAppNotification = inAppNotification
+ )
+
+ @Provides
+ fun provideUnRetweetStatusJob(
+ accountRepository: AccountRepository,
+ statusRepository: StatusRepository,
+ inAppNotification: InAppNotification
+ ): UnRetweetStatusJob = UnRetweetStatusJob(
+ accountRepository = accountRepository,
+ statusRepository = statusRepository,
+ inAppNotification = inAppNotification
+ )
+
+ @Provides
+ fun provideMastodonVoteJob(
+ accountRepository: AccountRepository,
+ statusRepository: StatusRepository,
+ inAppNotification: InAppNotification
+ ): MastodonVoteJob = MastodonVoteJob(
+ accountRepository = accountRepository,
+ statusRepository = statusRepository,
+ inAppNotification = inAppNotification
+ )
+
+ @Provides
+ fun provideRemoveDraftJob(
+ repository: DraftRepository
+ ): RemoveDraftJob = RemoveDraftJob(
+ repository = repository,
+ )
+
+ @Provides
+ fun provideSaveDraftJob(
+ repository: DraftRepository,
+ inAppNotification: InAppNotification
+ ): SaveDraftJob = SaveDraftJob(
+ repository = repository,
+ inAppNotification = inAppNotification
+ )
+
+ @Provides
+ fun provideDirectMessageDeleteJob(
+ repository: DirectMessageRepository,
+ accountRepository: AccountRepository
+ ): DirectMessageDeleteJob = DirectMessageDeleteJob(
+ repository = repository,
+ accountRepository = accountRepository
+ )
+
+ @Provides
+ fun provideDirectMessageFetchJob(
+ @ApplicationContext context: Context,
+ repository: DirectMessageRepository,
+ accountRepository: AccountRepository,
+ notificationManager: AppNotificationManager,
+ ): DirectMessageFetchJob = DirectMessageFetchJob(
+ applicationContext = context,
+ repository = repository,
+ accountRepository = accountRepository,
+ notificationManager = notificationManager
+ )
+
+ @Provides
+ fun provideTwitterDirectMessageSendJob(
+ @ApplicationContext context: Context,
+ accountRepository: AccountRepository,
+ notificationManager: AppNotificationManager,
+ fileResolver: FileResolver,
+ cacheDatabase: CacheDatabase,
+ ): TwitterDirectMessageSendJob = TwitterDirectMessageSendJob(
+ context = context,
+ accountRepository = accountRepository,
+ notificationManager = notificationManager,
+ fileResolver = fileResolver,
+ cacheDatabase = cacheDatabase
+ )
+
+ @Provides
+ fun provideTwitterComposeJob(
+ @ApplicationContext context: Context,
+ accountRepository: AccountRepository,
+ notificationManager: AppNotificationManager,
+ fileResolver: FileResolver,
+ cacheDatabase: CacheDatabase,
+ exifScrambler: ExifScrambler,
+ statusRepository: StatusRepository,
+ remoteNavigator: RemoteNavigator
+ ): TwitterComposeJob = TwitterComposeJob(
+ context = context,
+ accountRepository = accountRepository,
+ notificationManager = notificationManager,
+ fileResolver = fileResolver,
+ cacheDatabase = cacheDatabase,
+ exifScrambler = exifScrambler,
+ statusRepository = statusRepository,
+ remoteNavigator = remoteNavigator
+ )
+
+ @Provides
+ fun provideMastodonComposeJob(
+ @ApplicationContext context: Context,
+ accountRepository: AccountRepository,
+ notificationManager: AppNotificationManager,
+ fileResolver: FileResolver,
+ cacheDatabase: CacheDatabase,
+ exifScrambler: ExifScrambler,
+ remoteNavigator: RemoteNavigator
+ ): MastodonComposeJob = MastodonComposeJob(
+ context = context,
+ accountRepository = accountRepository,
+ notificationManager = notificationManager,
+ fileResolver = fileResolver,
+ cacheDatabase = cacheDatabase,
+ exifScrambler = exifScrambler,
+ remoteNavigator = remoteNavigator
+ )
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/di/TwidereModule.kt b/app/src/main/kotlin/com/twidere/twiderex/di/TwidereModule.kt
index 360af627e..fd78bd461 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/di/TwidereModule.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/di/TwidereModule.kt
@@ -20,14 +20,25 @@
*/
package com.twidere.twiderex.di
+import android.content.ContentResolver
import android.content.Context
+import androidx.core.app.NotificationManagerCompat
import androidx.datastore.core.DataStore
import androidx.work.WorkManager
import com.twidere.services.nitter.NitterService
import com.twidere.twiderex.action.ComposeAction
import com.twidere.twiderex.action.DirectMessageAction
import com.twidere.twiderex.db.CacheDatabase
+import com.twidere.twiderex.http.TwidereServiceFactory
+import com.twidere.twiderex.kmp.ExifScrambler
+import com.twidere.twiderex.kmp.FileResolver
+import com.twidere.twiderex.kmp.RemoteNavigator
+import com.twidere.twiderex.kmp.android.AndroidExifScrambler
+import com.twidere.twiderex.kmp.android.AndroidFileResolver
+import com.twidere.twiderex.kmp.android.AndroidNotificationManager
+import com.twidere.twiderex.kmp.android.AndroidRemoteNavigator
import com.twidere.twiderex.model.AccountPreferences
+import com.twidere.twiderex.notification.AppNotificationManager
import com.twidere.twiderex.notification.InAppNotification
import com.twidere.twiderex.preferences.proto.MiscPreferences
import com.twidere.twiderex.utils.PlatformResolver
@@ -62,7 +73,7 @@ object TwidereModule {
fun provideNitterService(preferences: DataStore): NitterService? {
return runBlocking {
preferences.data.first().nitterInstance.takeIf { it.isNotEmpty() }
- ?.let { NitterService(it.trimEnd('/')) }
+ ?.let { NitterService(it.trimEnd('/'), TwidereServiceFactory.createHttpClientFactory()) }
}
}
@@ -75,4 +86,25 @@ object TwidereModule {
@Provides
fun provideAccountPreferencesFactory(@ApplicationContext context: Context): AccountPreferences.Factory =
AccountPreferences.Factory(context)
+
+ @Provides
+ fun provideAppNotificationManager(@ApplicationContext context: Context, notificationManagerCompat: NotificationManagerCompat): AppNotificationManager = AndroidNotificationManager(
+ context = context,
+ notificationManagerCompat = notificationManagerCompat
+ )
+
+ @Provides
+ fun provideExifScrambler(@ApplicationContext context: Context): ExifScrambler = AndroidExifScrambler(
+ context = context
+ )
+
+ @Provides
+ fun provideFileResolver(contentResolver: ContentResolver): FileResolver = AndroidFileResolver(
+ contentResolver = contentResolver
+ )
+
+ @Provides
+ fun provideRemoteNavigator(@ApplicationContext context: Context): RemoteNavigator = AndroidRemoteNavigator(
+ context = context
+ )
}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/di/assisted/AssistedViewModel.kt b/app/src/main/kotlin/com/twidere/twiderex/di/assisted/AssistedViewModel.kt
index b7a24731e..85154c306 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/di/assisted/AssistedViewModel.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/di/assisted/AssistedViewModel.kt
@@ -41,7 +41,7 @@ inline fun assistedViewModel(
null
},
factory = object : ViewModelProvider.Factory {
- override fun create(modelClass: Class): T {
+ override fun create(modelClass: Class): T {
@Suppress("UNCHECKED_CAST")
return factory?.let { creator?.invoke(it) } as T
}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/extensions/ComposeExtensions.kt b/app/src/main/kotlin/com/twidere/twiderex/extensions/ComposeExtensions.kt
index b27d57ada..7c6e785df 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/extensions/ComposeExtensions.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/extensions/ComposeExtensions.kt
@@ -40,7 +40,7 @@ inline fun viewModel(
null
},
factory = object : ViewModelProvider.Factory {
- override fun create(modelClass: Class): T {
+ override fun create(modelClass: Class): T {
@Suppress("UNCHECKED_CAST")
return creator?.invoke() as T
}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/extensions/ContextExtensions.kt b/app/src/main/kotlin/com/twidere/twiderex/extensions/ContextExtensions.kt
index 35cb7aace..c0bc2fe10 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/extensions/ContextExtensions.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/extensions/ContextExtensions.kt
@@ -34,14 +34,16 @@ fun Context.checkAnySelfPermissionsGranted(vararg permissions: String): Boolean
return permissions.any { ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED }
}
-fun Context.shareText(content: String) {
+fun Context.shareText(content: String, fromOutsideOfActivity: Boolean = false) {
startActivity(
Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, content)
type = "text/plain"
}.let {
- Intent.createChooser(it, null)
+ Intent.createChooser(it, null).apply {
+ if (fromOutsideOfActivity) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
}
)
}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/extensions/FlowExtensions.kt b/app/src/main/kotlin/com/twidere/twiderex/extensions/FlowExtensions.kt
new file mode 100644
index 000000000..b3be1c2ce
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/extensions/FlowExtensions.kt
@@ -0,0 +1,37 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.extensions
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.lifecycle.flowWithLifecycle
+import kotlinx.coroutines.flow.Flow
+
+@Composable
+fun Flow.observeAsState(initial: T): State {
+ val lifecycleOwner = LocalLifecycleOwner.current
+ return remember(this, lifecycleOwner) {
+ flowWithLifecycle(lifecycleOwner.lifecycle)
+ }.collectAsState(initial = initial)
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/extensions/MastodonExtensions.kt b/app/src/main/kotlin/com/twidere/twiderex/extensions/MastodonExtensions.kt
index 7c2d76e75..77f224893 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/extensions/MastodonExtensions.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/extensions/MastodonExtensions.kt
@@ -24,25 +24,25 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
-import com.twidere.services.mastodon.model.Visibility
import com.twidere.twiderex.R
+import com.twidere.twiderex.model.enums.MastodonVisibility
@Composable
-fun Visibility.icon(): Painter {
+fun MastodonVisibility.icon(): Painter {
return when (this) {
- Visibility.Public -> painterResource(id = R.drawable.ic_globe)
- Visibility.Unlisted -> painterResource(id = R.drawable.ic_lock_open)
- Visibility.Private -> painterResource(id = R.drawable.ic_lock)
- Visibility.Direct -> painterResource(id = R.drawable.ic_mail)
+ MastodonVisibility.Public -> painterResource(id = R.drawable.ic_globe)
+ MastodonVisibility.Unlisted -> painterResource(id = R.drawable.ic_lock_open)
+ MastodonVisibility.Private -> painterResource(id = R.drawable.ic_lock)
+ MastodonVisibility.Direct -> painterResource(id = R.drawable.ic_mail)
}
}
@Composable
-fun Visibility.stringName(): String {
+fun MastodonVisibility.stringName(): String {
return when (this) {
- Visibility.Public -> stringResource(id = R.string.scene_compose_visibility_public)
- Visibility.Unlisted -> stringResource(id = R.string.scene_compose_visibility_unlisted)
- Visibility.Private -> stringResource(id = R.string.scene_compose_visibility_private)
- Visibility.Direct -> stringResource(id = R.string.scene_compose_visibility_direct)
+ MastodonVisibility.Public -> stringResource(id = R.string.scene_compose_visibility_public)
+ MastodonVisibility.Unlisted -> stringResource(id = R.string.scene_compose_visibility_unlisted)
+ MastodonVisibility.Private -> stringResource(id = R.string.scene_compose_visibility_private)
+ MastodonVisibility.Direct -> stringResource(id = R.string.scene_compose_visibility_direct)
}
}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/extensions/PagingExtensions.kt b/app/src/main/kotlin/com/twidere/twiderex/extensions/PagingExtensions.kt
index cdef9f589..ec14e0e8a 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/extensions/PagingExtensions.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/extensions/PagingExtensions.kt
@@ -24,7 +24,7 @@ import androidx.paging.Pager
import androidx.paging.map
import com.twidere.twiderex.db.model.DbPagingTimelineWithStatus
import com.twidere.twiderex.model.MicroBlogKey
-import com.twidere.twiderex.model.ui.UiStatus.Companion.toUi
+import com.twidere.twiderex.model.transform.toUi
import kotlinx.coroutines.flow.map
fun Pager.toUi(accountKey: MicroBlogKey) =
diff --git a/app/src/main/kotlin/com/twidere/twiderex/http/TwidereHttpConfigProvider.kt b/app/src/main/kotlin/com/twidere/twiderex/http/TwidereHttpConfigProvider.kt
new file mode 100644
index 000000000..f695d5bb2
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/http/TwidereHttpConfigProvider.kt
@@ -0,0 +1,55 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.http
+
+import androidx.datastore.core.DataStore
+import com.twidere.services.http.HttpConfigProvider
+import com.twidere.services.http.config.HttpConfig
+import com.twidere.services.proxy.ProxyConfig
+import com.twidere.twiderex.preferences.proto.MiscPreferences
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.runBlocking
+import javax.inject.Inject
+
+class TwidereHttpConfigProvider @Inject constructor(
+ private val miscPreferences: DataStore
+) : HttpConfigProvider {
+ override fun provideConfig(): HttpConfig {
+ return runBlocking {
+ miscPreferences.data.map {
+ HttpConfig(
+ proxyConfig = ProxyConfig(
+ enable = it.useProxy,
+ server = it.proxyServer,
+ port = it.proxyPort,
+ userName = it.proxyUserName,
+ password = it.proxyPassword,
+ type = when (it.proxyType) {
+ MiscPreferences.ProxyType.REVERSE -> ProxyConfig.Type.REVERSE
+ else -> ProxyConfig.Type.HTTP
+ }
+ )
+ )
+ }.first()
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/http/TwidereNetworkImageLoader.kt b/app/src/main/kotlin/com/twidere/twiderex/http/TwidereNetworkImageLoader.kt
new file mode 100644
index 000000000..b7b885732
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/http/TwidereNetworkImageLoader.kt
@@ -0,0 +1,96 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.http
+
+import android.content.Context
+import android.net.Uri
+import coil.ImageLoader
+import coil.bitmap.BitmapPool
+import coil.memory.MemoryCache
+import coil.request.DefaultRequestOptions
+import coil.request.Disposable
+import coil.request.ImageRequest
+import coil.request.ImageResult
+import com.twidere.services.http.authorization.OAuth1Authorization
+import com.twidere.twiderex.model.AccountDetails
+import com.twidere.twiderex.model.cred.OAuthCredentials
+import com.twidere.twiderex.model.enums.PlatformType
+import okhttp3.Headers
+import okhttp3.Request
+import java.net.URL
+
+class TwidereNetworkImageLoader(
+ private val realImageLoader: ImageLoader,
+ private val context: Context,
+ private val account: AccountDetails?
+) : ImageLoader {
+ private val twitterTonApiHost = "ton.twitter.com"
+ override val bitmapPool: BitmapPool
+ get() = realImageLoader.bitmapPool
+ override val defaults: DefaultRequestOptions
+ get() = realImageLoader.defaults
+ override val memoryCache: MemoryCache
+ get() = realImageLoader.memoryCache
+
+ override fun enqueue(request: ImageRequest): Disposable {
+ return realImageLoader.enqueue(handleRequest(request))
+ }
+
+ override suspend fun execute(request: ImageRequest): ImageResult {
+ return realImageLoader.execute(handleRequest(request))
+ }
+
+ override fun newBuilder(): ImageLoader.Builder {
+ return ImageLoader.Builder(context)
+ }
+
+ override fun shutdown() {
+ realImageLoader.shutdown()
+ }
+
+ private fun handleRequest(request: ImageRequest): ImageRequest {
+ var data = request.data
+ // ton.twitter.com must be retrieved via an authenticated
+ if (data is String) data = Uri.parse(data)
+ return if (data is Uri && twitterTonApiHost == data.host && account?.type == PlatformType.Twitter) {
+ val auth = (account.credentials as OAuthCredentials).let {
+ OAuth1Authorization(
+ consumerKey = it.consumer_key,
+ consumerSecret = it.consumer_secret,
+ accessToken = it.access_token,
+ accessSecret = it.access_token_secret,
+ )
+ }
+ request.newBuilder(
+ request.context
+ ).headers(
+ headers = Headers.headersOf(
+ "Authorization",
+ auth.getAuthorizationHeader(Request.Builder().url(URL(data.toString())).build())
+ )
+ ).build()
+ } else {
+ request.newBuilder(request.context)
+ .data(data)
+ .build()
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/http/TwidereServiceFactory.kt b/app/src/main/kotlin/com/twidere/twiderex/http/TwidereServiceFactory.kt
new file mode 100644
index 000000000..dcebf8e12
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/http/TwidereServiceFactory.kt
@@ -0,0 +1,80 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.http
+
+import com.twidere.services.http.HttpClientFactory
+import com.twidere.services.http.config.HttpConfigClientFactory
+import com.twidere.services.mastodon.MastodonService
+import com.twidere.services.microblog.MicroBlogService
+import com.twidere.services.twitter.TwitterService
+import com.twidere.twiderex.model.cred.Credentials
+import com.twidere.twiderex.model.cred.OAuth2Credentials
+import com.twidere.twiderex.model.cred.OAuthCredentials
+import com.twidere.twiderex.model.enums.PlatformType
+
+class TwidereServiceFactory(private val configProvider: TwidereHttpConfigProvider) {
+
+ companion object {
+ private var instance: TwidereServiceFactory? = null
+
+ fun initiate(configProvider: TwidereHttpConfigProvider) {
+ instance = TwidereServiceFactory(configProvider)
+ }
+
+ fun createApiService(type: PlatformType, credentials: Credentials, host: String = ""): MicroBlogService {
+ return instance?.let {
+ when (type) {
+ PlatformType.Twitter -> {
+ credentials.let {
+ it as OAuthCredentials
+ }.let {
+ TwitterService(
+ consumer_key = it.consumer_key,
+ consumer_secret = it.consumer_secret,
+ access_token = it.access_token,
+ access_token_secret = it.access_token_secret,
+ httpClientFactory = createHttpClientFactory()
+ )
+ }
+ }
+ PlatformType.StatusNet -> TODO()
+ PlatformType.Fanfou -> TODO()
+ PlatformType.Mastodon ->
+ credentials.let {
+ it as OAuth2Credentials
+ }.let {
+ MastodonService(
+ host,
+ it.access_token,
+ httpClientFactory = createHttpClientFactory()
+ )
+ }
+ }
+ } ?: throw Error("Factory needs to be initiate")
+ }
+
+ fun createHttpClientFactory(): HttpClientFactory {
+ return instance?.let {
+ HttpConfigClientFactory(it.configProvider)
+ } ?: throw Error("Factory needs to be initiate")
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/http/TwidereServiceInitializer.kt b/app/src/main/kotlin/com/twidere/twiderex/http/TwidereServiceInitializer.kt
new file mode 100644
index 000000000..2f5dfe567
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/http/TwidereServiceInitializer.kt
@@ -0,0 +1,43 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.http
+
+import android.content.Context
+import androidx.startup.Initializer
+import com.twidere.twiderex.di.InitializerEntryPoint
+import javax.inject.Inject
+
+class TwidereserviceInitializerHolder
+
+class TwidereServiceInitializer : Initializer {
+ @Inject
+ lateinit var configProvider: TwidereHttpConfigProvider
+
+ override fun create(context: Context): TwidereserviceInitializerHolder {
+ InitializerEntryPoint.resolve(context).inject(this)
+ TwidereServiceFactory.initiate(configProvider)
+ return TwidereserviceInitializerHolder()
+ }
+
+ override fun dependencies(): MutableList>> {
+ return mutableListOf()
+ }
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/jobs/common/DownloadMediaJob.kt b/app/src/main/kotlin/com/twidere/twiderex/jobs/common/DownloadMediaJob.kt
new file mode 100644
index 000000000..ef7cf3729
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/jobs/common/DownloadMediaJob.kt
@@ -0,0 +1,54 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.jobs.common
+
+import com.twidere.services.microblog.DownloadMediaService
+import com.twidere.twiderex.R
+import com.twidere.twiderex.kmp.FileResolver
+import com.twidere.twiderex.model.MicroBlogKey
+import com.twidere.twiderex.notification.InAppNotification
+import com.twidere.twiderex.repository.AccountRepository
+
+class DownloadMediaJob(
+ private val accountRepository: AccountRepository,
+ private val inAppNotification: InAppNotification,
+ private val fileResolver: FileResolver,
+) {
+ suspend fun execute(
+ target: String,
+ source: String,
+ accountKey: MicroBlogKey,
+ ) {
+ val accountDetails = accountKey.let {
+ accountRepository.findByAccountKey(accountKey = it)
+ }?.let {
+ accountRepository.getAccountDetails(it)
+ } ?: throw Error("Can't find any account matches:$$accountKey")
+ val service = accountDetails.service
+ if (service !is DownloadMediaService) {
+ throw Error("Service must be DownloadMediaService")
+ }
+ fileResolver.openOutputStream(target)?.use {
+ service.download(target = source).copyTo(it)
+ } ?: throw Error("Download failed")
+ inAppNotification.show(R.string.common_controls_actions_save)
+ }
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/jobs/common/NotificationJob.kt b/app/src/main/kotlin/com/twidere/twiderex/jobs/common/NotificationJob.kt
new file mode 100644
index 000000000..55c036a0e
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/jobs/common/NotificationJob.kt
@@ -0,0 +1,209 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.jobs.common
+
+import android.content.Context
+import androidx.core.graphics.drawable.toBitmap
+import androidx.core.text.HtmlCompat
+import coil.Coil
+import coil.request.ImageRequest
+import coil.request.SuccessResult
+import com.twidere.twiderex.R
+import com.twidere.twiderex.model.AccountDetails
+import com.twidere.twiderex.model.enums.MastodonStatusType
+import com.twidere.twiderex.model.enums.PlatformType
+import com.twidere.twiderex.model.enums.ReferenceType
+import com.twidere.twiderex.model.ui.UiStatus
+import com.twidere.twiderex.navigation.RootDeepLinksRoute
+import com.twidere.twiderex.notification.AppNotification
+import com.twidere.twiderex.notification.AppNotificationManager
+import com.twidere.twiderex.notification.NotificationChannelSpec
+import com.twidere.twiderex.notification.notificationChannelId
+import com.twidere.twiderex.repository.AccountRepository
+import com.twidere.twiderex.repository.NotificationRepository
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.joinAll
+import kotlinx.coroutines.launch
+
+class NotificationJob(
+ private val applicationContext: Context,
+ private val repository: NotificationRepository,
+ private val accountRepository: AccountRepository,
+ private val notificationManager: AppNotificationManager,
+) {
+ suspend fun execute(enableNotification: Boolean) = coroutineScope {
+ if (!enableNotification) {
+ accountRepository.getAccounts().map { accountRepository.getAccountDetails(it) }
+ .filter {
+ it.preferences.isNotificationEnabled.first()
+ }
+ .map { account ->
+ launch {
+ val activities = try {
+ repository.activities(account)
+ } catch (e: Throwable) {
+ // Ignore any exception cause there's no needs ot handle it
+ emptyList()
+ }
+ activities.forEach { status ->
+ notify(account, status)
+ }
+ }
+ }.joinAll()
+ }
+ }
+
+ private suspend fun notify(account: AccountDetails, status: UiStatus) {
+ val notificationId = "${account.accountKey}_${status.statusKey}"
+ val builder = AppNotification
+ .Builder(
+ account.accountKey.notificationChannelId(
+ NotificationChannelSpec.ContentInteractions.id
+ )
+ )
+
+ val notificationData = when (status.platformType) {
+ PlatformType.Twitter -> {
+ NotificationData(
+ title = applicationContext.getString(
+ R.string.common_notification_mentions,
+ status.user.displayName
+ ),
+ htmlContent = status.htmlText,
+ deepLink = RootDeepLinksRoute.Twitter.Status(status.statusId),
+ profileImage = status.user.profileImage,
+ )
+ }
+ PlatformType.StatusNet -> TODO()
+ PlatformType.Fanfou -> TODO()
+ PlatformType.Mastodon -> {
+ generateMastodonNotificationData(
+ status,
+ )
+ }
+ }
+ if (notificationData != null) {
+ builder.setContentTitle(notificationData.title)
+ if (notificationData.htmlContent != null) {
+ val html = HtmlCompat.fromHtml(
+ notificationData.htmlContent,
+ HtmlCompat.FROM_HTML_MODE_COMPACT
+ )
+ builder
+ .setContentText(html)
+ }
+ if (notificationData.deepLink != null) {
+ builder.setDeepLink(notificationData.deepLink)
+ }
+ if (notificationData.profileImage != null) {
+ val result = Coil.execute(
+ ImageRequest.Builder(applicationContext)
+ .data(notificationData.profileImage)
+ .build()
+ )
+ if (result is SuccessResult) {
+ builder.setLargeIcon(result.drawable.toBitmap())
+ }
+ }
+ notificationManager.notify(notificationId.hashCode(), builder.build())
+ }
+ }
+
+ private fun generateMastodonNotificationData(
+ status: UiStatus
+ ): NotificationData? {
+ val actualStatus = status.referenceStatus[ReferenceType.MastodonNotification]
+ if (status.mastodonExtra == null || actualStatus == null) {
+ return null
+ }
+ return when (status.mastodonExtra.type) {
+ MastodonStatusType.Status -> null
+ MastodonStatusType.NotificationFollow -> {
+ NotificationData(
+ title = applicationContext.getString(
+ R.string.common_notification_follow,
+ actualStatus.user.displayName
+ ),
+ deepLink = RootDeepLinksRoute.User(actualStatus.user.userKey),
+ profileImage = actualStatus.user.profileImage,
+ )
+ }
+ MastodonStatusType.NotificationFollowRequest -> {
+ NotificationData(
+ title = applicationContext.getString(
+ R.string.common_notification_follow_request,
+ actualStatus.user.displayName
+ ),
+ deepLink = RootDeepLinksRoute.User(actualStatus.user.userKey)
+ )
+ }
+ MastodonStatusType.NotificationMention -> {
+ NotificationData(
+ title = applicationContext.getString(
+ R.string.common_notification_mentions,
+ actualStatus.user.displayName
+ ),
+ htmlContent = actualStatus.htmlText,
+ deepLink = RootDeepLinksRoute.Status(actualStatus.statusKey),
+ profileImage = actualStatus.user.profileImage,
+ )
+ }
+ MastodonStatusType.NotificationReblog -> {
+ NotificationData(
+ title = applicationContext.getString(
+ R.string.common_notification_reblog,
+ actualStatus.user.displayName
+ ),
+ deepLink = RootDeepLinksRoute.Status(actualStatus.statusKey),
+ profileImage = actualStatus.user.profileImage,
+ )
+ }
+ MastodonStatusType.NotificationFavourite -> {
+ NotificationData(
+ title = applicationContext.getString(
+ R.string.common_notification_favourite,
+ actualStatus.user.displayName
+ ),
+ deepLink = RootDeepLinksRoute.Status(actualStatus.statusKey),
+ profileImage = actualStatus.user.profileImage,
+ )
+ }
+ MastodonStatusType.NotificationPoll -> {
+ NotificationData(
+ title = applicationContext.getString(
+ R.string.common_notification_poll,
+ ),
+ deepLink = RootDeepLinksRoute.Status(actualStatus.statusKey),
+ profileImage = actualStatus.user.profileImage,
+ )
+ }
+ MastodonStatusType.NotificationStatus -> null
+ }
+ }
+}
+
+private data class NotificationData(
+ val title: String,
+ val profileImage: Any? = null,
+ val htmlContent: String? = null,
+ val deepLink: String? = null,
+)
diff --git a/app/src/main/kotlin/com/twidere/twiderex/jobs/common/ShareMediaJob.kt b/app/src/main/kotlin/com/twidere/twiderex/jobs/common/ShareMediaJob.kt
new file mode 100644
index 000000000..96af576a9
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/jobs/common/ShareMediaJob.kt
@@ -0,0 +1,40 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.jobs.common
+
+import com.twidere.twiderex.kmp.FileResolver
+import com.twidere.twiderex.kmp.RemoteNavigator
+
+class ShareMediaJob(
+ private val fileResolver: FileResolver,
+ private val remoteNavigator: RemoteNavigator
+) {
+ fun execute(target: String) {
+ fileResolver.getMimeType(target)?.let { type ->
+ remoteNavigator.shareMedia(
+ filePath = target,
+ mimeType = type,
+ fromBackground = true
+ )
+ true
+ } ?: throw Error("Unresolved file:$target")
+ }
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/jobs/compose/ComposeJob.kt b/app/src/main/kotlin/com/twidere/twiderex/jobs/compose/ComposeJob.kt
new file mode 100644
index 000000000..4fe75382b
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/jobs/compose/ComposeJob.kt
@@ -0,0 +1,118 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.jobs.compose
+
+import android.content.Context
+import com.twidere.services.microblog.MicroBlogService
+import com.twidere.twiderex.R
+import com.twidere.twiderex.kmp.ExifScrambler
+import com.twidere.twiderex.kmp.RemoteNavigator
+import com.twidere.twiderex.model.MicroBlogKey
+import com.twidere.twiderex.model.job.ComposeData
+import com.twidere.twiderex.model.ui.UiStatus
+import com.twidere.twiderex.navigation.RootDeepLinksRoute
+import com.twidere.twiderex.notification.AppNotification
+import com.twidere.twiderex.notification.AppNotificationManager
+import com.twidere.twiderex.notification.NotificationChannelSpec
+import com.twidere.twiderex.repository.AccountRepository
+import com.twidere.twiderex.viewmodel.compose.ComposeType
+import kotlin.math.roundToInt
+
+abstract class ComposeJob(
+ private val applicationContext: Context,
+ private val accountRepository: AccountRepository,
+ private val notificationManager: AppNotificationManager,
+ private val exifScrambler: ExifScrambler,
+ private val remoteNavigator: RemoteNavigator,
+) {
+ suspend fun execute(composeData: ComposeData, accountKey: MicroBlogKey) {
+ val builder = AppNotification
+ .Builder(NotificationChannelSpec.BackgroundProgresses.id)
+ .setContentTitle(applicationContext.getString(R.string.common_alerts_tweet_sending_title))
+ .setOngoing(true)
+ .setSilent(true)
+ .setProgress(100, 0, false)
+ val accountDetails = accountKey.let {
+ accountRepository.findByAccountKey(accountKey = it)
+ }?.let {
+ accountRepository.getAccountDetails(it)
+ } ?: throw Error("Can't find any account matches:$$accountKey")
+ val notificationId = composeData.draftId.hashCode()
+ @Suppress("UNCHECKED_CAST")
+ val service = accountDetails.service as T
+ notificationManager.notify(notificationId, builder.build())
+
+ try {
+ val mediaIds = arrayListOf()
+ val images = composeData.images
+ images.forEachIndexed { index, uri ->
+ val scramblerUri = exifScrambler.removeExifData(uri)
+ val id = uploadImage(uri, scramblerUri, service)
+ id?.let { mediaIds.add(it) }
+ builder.setProgress(
+ 100,
+ (99f * index.toFloat() / composeData.images.size.toFloat()).roundToInt(),
+ false
+ )
+ notificationManager.notify(notificationId, builder.build())
+ exifScrambler.deleteCacheFile(scramblerUri)
+ }
+ builder.setProgress(100, 99, false)
+ notificationManager.notify(notificationId, builder.build())
+ val status = compose(service, composeData, accountKey, mediaIds)
+ builder.setOngoing(false)
+ .setProgress(0, 0, false)
+ .setSilent(false)
+ .setContentTitle(applicationContext.getString(R.string.common_alerts_tweet_sent_title))
+ notificationManager.notifyTransient(notificationId, builder.build())
+ if (composeData.isThreadMode) {
+ // open compose scene in thread mode
+ remoteNavigator.openDeepLink(
+ deeplink = RootDeepLinksRoute.Compose(ComposeType.Thread, status.statusKey),
+ fromBackground = true
+ )
+ }
+ } catch (e: Throwable) {
+ e.printStackTrace()
+ builder.setOngoing(false)
+ .setProgress(0, 0, false)
+ .setSilent(false)
+ .setContentTitle(applicationContext.getString(R.string.common_alerts_tweet_fail_title))
+ .setContentText(composeData.content)
+ .setDeepLink(RootDeepLinksRoute.Draft(composeData.draftId))
+ notificationManager.notify(notificationId, builder.build())
+ throw e
+ }
+ }
+
+ protected abstract suspend fun compose(
+ service: T,
+ composeData: ComposeData,
+ accountKey: MicroBlogKey,
+ mediaIds: ArrayList
+ ): UiStatus
+
+ protected abstract suspend fun uploadImage(
+ originUri: String,
+ scramblerUri: String,
+ service: T
+ ): String?
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/jobs/compose/MastodonComposeJob.kt b/app/src/main/kotlin/com/twidere/twiderex/jobs/compose/MastodonComposeJob.kt
new file mode 100644
index 000000000..65b49d11b
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/jobs/compose/MastodonComposeJob.kt
@@ -0,0 +1,105 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.jobs.compose
+
+import android.content.Context
+import com.twidere.services.mastodon.MastodonService
+import com.twidere.services.mastodon.model.PostPoll
+import com.twidere.services.mastodon.model.PostStatus
+import com.twidere.services.mastodon.model.Visibility
+import com.twidere.twiderex.db.CacheDatabase
+import com.twidere.twiderex.db.mapper.toDbStatusWithReference
+import com.twidere.twiderex.db.model.saveToDb
+import com.twidere.twiderex.kmp.ExifScrambler
+import com.twidere.twiderex.kmp.FileResolver
+import com.twidere.twiderex.kmp.RemoteNavigator
+import com.twidere.twiderex.model.MicroBlogKey
+import com.twidere.twiderex.model.enums.MastodonVisibility
+import com.twidere.twiderex.model.job.ComposeData
+import com.twidere.twiderex.model.transform.toUi
+import com.twidere.twiderex.model.ui.UiStatus
+import com.twidere.twiderex.notification.AppNotificationManager
+import com.twidere.twiderex.repository.AccountRepository
+import com.twidere.twiderex.viewmodel.compose.ComposeType
+import java.io.File
+import java.net.URI
+
+class MastodonComposeJob(
+ context: Context,
+ accountRepository: AccountRepository,
+ notificationManager: AppNotificationManager,
+ exifScrambler: ExifScrambler,
+ remoteNavigator: RemoteNavigator,
+ private val fileResolver: FileResolver,
+ private val cacheDatabase: CacheDatabase,
+) : ComposeJob(
+ context,
+ accountRepository,
+ notificationManager,
+ exifScrambler,
+ remoteNavigator
+) {
+ override suspend fun compose(
+ service: MastodonService,
+ composeData: ComposeData,
+ accountKey: MicroBlogKey,
+ mediaIds: ArrayList
+ ): UiStatus {
+ val result = service.compose(
+ PostStatus(
+ status = composeData.content,
+ inReplyToID = if (composeData.composeType == ComposeType.Reply || composeData.composeType == ComposeType.Thread) composeData.statusKey?.id else null,
+ mediaIDS = mediaIds,
+ sensitive = composeData.isSensitive,
+ spoilerText = composeData.contentWarningText,
+ visibility = when (composeData.visibility) {
+ MastodonVisibility.Public, null -> Visibility.Public
+ MastodonVisibility.Unlisted -> Visibility.Unlisted
+ MastodonVisibility.Private -> Visibility.Private
+ MastodonVisibility.Direct -> Visibility.Direct
+ },
+ poll = composeData.voteOptions?.let {
+ PostPoll(
+ options = composeData.voteOptions,
+ expiresIn = composeData.voteExpired?.value,
+ multiple = composeData.voteMultiple
+ )
+ }
+ )
+ ).toDbStatusWithReference(accountKey)
+ listOf(result).saveToDb(cacheDatabase)
+ return result.toUi(accountKey)
+ }
+
+ override suspend fun uploadImage(
+ originUri: String,
+ scramblerUri: String,
+ service: MastodonService
+ ): String? {
+ val id = fileResolver.openInputStream(scramblerUri)?.use { input ->
+ service.upload(
+ input,
+ URI.create(originUri).path?.let { File(it).name }?.takeIf { it.isNotEmpty() } ?: "file"
+ )
+ } ?: throw Error()
+ return id.id
+ }
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/jobs/compose/TwitterComposeJob.kt b/app/src/main/kotlin/com/twidere/twiderex/jobs/compose/TwitterComposeJob.kt
new file mode 100644
index 000000000..b629de39e
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/jobs/compose/TwitterComposeJob.kt
@@ -0,0 +1,103 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.jobs.compose
+
+import android.content.Context
+import com.twidere.services.twitter.TwitterService
+import com.twidere.twiderex.db.CacheDatabase
+import com.twidere.twiderex.db.mapper.toDbStatusWithReference
+import com.twidere.twiderex.db.model.saveToDb
+import com.twidere.twiderex.kmp.ExifScrambler
+import com.twidere.twiderex.kmp.FileResolver
+import com.twidere.twiderex.kmp.RemoteNavigator
+import com.twidere.twiderex.model.MicroBlogKey
+import com.twidere.twiderex.model.job.ComposeData
+import com.twidere.twiderex.model.transform.toUi
+import com.twidere.twiderex.model.ui.UiStatus
+import com.twidere.twiderex.notification.AppNotificationManager
+import com.twidere.twiderex.repository.AccountRepository
+import com.twidere.twiderex.repository.StatusRepository
+import com.twidere.twiderex.viewmodel.compose.ComposeType
+
+class TwitterComposeJob constructor(
+ context: Context,
+ accountRepository: AccountRepository,
+ notificationManager: AppNotificationManager,
+ exifScrambler: ExifScrambler,
+ remoteNavigator: RemoteNavigator,
+ private val statusRepository: StatusRepository,
+ private val fileResolver: FileResolver,
+ private val cacheDatabase: CacheDatabase,
+) : ComposeJob(
+ context,
+ accountRepository,
+ notificationManager,
+ exifScrambler,
+ remoteNavigator
+) {
+ override suspend fun compose(
+ service: TwitterService,
+ composeData: ComposeData,
+ accountKey: MicroBlogKey,
+ mediaIds: ArrayList
+ ): UiStatus {
+ val lat = composeData.lat
+ val long = composeData.long
+ val content = composeData.content.let {
+ if (composeData.composeType == ComposeType.Quote && composeData.statusKey != null) {
+ val status = statusRepository.loadFromCache(
+ composeData.statusKey,
+ accountKey = accountKey
+ )
+ it + " ${status?.generateShareLink()}"
+ } else {
+ it
+ }
+ }
+ val result = service.update(
+ content,
+ media_ids = mediaIds,
+ in_reply_to_status_id = if (composeData.composeType == ComposeType.Reply || composeData.composeType == ComposeType.Thread) composeData.statusKey?.id else null,
+ repost_status_id = if (composeData.composeType == ComposeType.Quote) composeData.statusKey?.id else null,
+ lat = lat,
+ long = long,
+ exclude_reply_user_ids = composeData.excludedReplyUserIds
+ ).toDbStatusWithReference(accountKey)
+ listOf(result).saveToDb(cacheDatabase)
+ return result.toUi(accountKey)
+ }
+
+ override suspend fun uploadImage(
+ originUri: String,
+ scramblerUri: String,
+ service: TwitterService
+ ): String {
+ val type = fileResolver.getMimeType(originUri)
+ val size = fileResolver.getFileSize(scramblerUri)
+ return fileResolver.openInputStream(scramblerUri)?.use {
+ service.uploadFile(
+ it,
+ type ?: "image/*",
+ size ?: it.available().toLong()
+ )
+ } ?: throw Error()
+ }
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/jobs/database/DeleteDbStatusJob.kt b/app/src/main/kotlin/com/twidere/twiderex/jobs/database/DeleteDbStatusJob.kt
new file mode 100644
index 000000000..66a1cce13
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/jobs/database/DeleteDbStatusJob.kt
@@ -0,0 +1,32 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.jobs.database
+
+import com.twidere.twiderex.model.MicroBlogKey
+import com.twidere.twiderex.repository.StatusRepository
+
+class DeleteDbStatusJob(
+ private val statusRepository: StatusRepository
+) {
+ suspend fun execute(statusKey: MicroBlogKey) {
+ statusRepository.removeStatus(statusKey)
+ }
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/jobs/dm/DirectMessageDeleteJob.kt b/app/src/main/kotlin/com/twidere/twiderex/jobs/dm/DirectMessageDeleteJob.kt
new file mode 100644
index 000000000..21ff36fb7
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/jobs/dm/DirectMessageDeleteJob.kt
@@ -0,0 +1,47 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.jobs.dm
+
+import com.twidere.services.microblog.DirectMessageService
+import com.twidere.twiderex.model.MicroBlogKey
+import com.twidere.twiderex.model.job.DirectMessageDeleteData
+import com.twidere.twiderex.repository.AccountRepository
+import com.twidere.twiderex.repository.DirectMessageRepository
+
+class DirectMessageDeleteJob(
+ private val repository: DirectMessageRepository,
+ private val accountRepository: AccountRepository,
+) {
+ suspend fun execute(deleteData: DirectMessageDeleteData, accountKey: MicroBlogKey) {
+ val accountDetails = accountKey.let {
+ accountRepository.findByAccountKey(accountKey = it)
+ }?.let {
+ accountRepository.getAccountDetails(it)
+ } ?: throw Error("Can't find any account matches:$$accountKey")
+ repository.deleteMessage(
+ accountKey = deleteData.accountKey,
+ conversationKey = deleteData.conversationKey,
+ messageId = deleteData.messageId,
+ messageKey = deleteData.messageKey,
+ service = accountDetails.service as DirectMessageService
+ )
+ }
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/jobs/dm/DirectMessageFetchJob.kt b/app/src/main/kotlin/com/twidere/twiderex/jobs/dm/DirectMessageFetchJob.kt
new file mode 100644
index 000000000..55206df52
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/jobs/dm/DirectMessageFetchJob.kt
@@ -0,0 +1,72 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.jobs.dm
+
+import android.content.Context
+import com.twidere.services.microblog.DirectMessageService
+import com.twidere.services.microblog.LookupService
+import com.twidere.twiderex.R
+import com.twidere.twiderex.model.AccountDetails
+import com.twidere.twiderex.model.ui.UiDMConversationWithLatestMessage
+import com.twidere.twiderex.navigation.RootDeepLinksRoute
+import com.twidere.twiderex.notification.AppNotification
+import com.twidere.twiderex.notification.AppNotificationManager
+import com.twidere.twiderex.notification.NotificationChannelSpec
+import com.twidere.twiderex.notification.notificationChannelId
+import com.twidere.twiderex.repository.AccountRepository
+import com.twidere.twiderex.repository.DirectMessageRepository
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.firstOrNull
+
+class DirectMessageFetchJob(
+ private val applicationContext: Context,
+ private val repository: DirectMessageRepository,
+ private val accountRepository: AccountRepository,
+ private val notificationManager: AppNotificationManager,
+) {
+ suspend fun execute() {
+ accountRepository.activeAccount.firstOrNull()?.takeIf {
+ accountRepository.getAccountPreferences(it.accountKey).isNotificationEnabled.first()
+ }?.let { account ->
+ val result = repository.checkNewMessages(
+ accountKey = account.accountKey,
+ service = account.service as DirectMessageService,
+ lookupService = account.service as LookupService
+ )
+ result.forEach {
+ notification(account = account, message = it)
+ }
+ }
+ }
+
+ private fun notification(account: AccountDetails, message: UiDMConversationWithLatestMessage) {
+ val builder = AppNotification
+ .Builder(
+ account.accountKey.notificationChannelId(
+ NotificationChannelSpec.ContentMessages.id
+ )
+ )
+ .setContentTitle(applicationContext.getString(R.string.common_notification_messages_title))
+ .setContentText(applicationContext.getString(R.string.common_notification_messages_content, message.latestMessage.sender.displayName))
+ .setDeepLink(RootDeepLinksRoute.Conversation(message.conversation.conversationKey))
+ notificationManager.notify(message.latestMessage.messageKey.hashCode(), builder.build())
+ }
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/jobs/dm/DirectMessageSendJob.kt b/app/src/main/kotlin/com/twidere/twiderex/jobs/dm/DirectMessageSendJob.kt
new file mode 100644
index 000000000..244d773c3
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/jobs/dm/DirectMessageSendJob.kt
@@ -0,0 +1,207 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.jobs.dm
+
+import android.content.Context
+import android.graphics.BitmapFactory
+import androidx.room.withTransaction
+import com.twidere.services.microblog.MicroBlogService
+import com.twidere.twiderex.R
+import com.twidere.twiderex.db.CacheDatabase
+import com.twidere.twiderex.db.model.DbDMEvent
+import com.twidere.twiderex.db.model.DbDMEvent.Companion.saveToDb
+import com.twidere.twiderex.db.model.DbDMEventWithAttachments
+import com.twidere.twiderex.db.model.DbDMEventWithAttachments.Companion.saveToDb
+import com.twidere.twiderex.db.model.DbMedia
+import com.twidere.twiderex.kmp.FileResolver
+import com.twidere.twiderex.model.AccountDetails
+import com.twidere.twiderex.model.MicroBlogKey
+import com.twidere.twiderex.model.enums.MediaType
+import com.twidere.twiderex.model.job.DirectMessageSendData
+import com.twidere.twiderex.navigation.RootDeepLinksRoute
+import com.twidere.twiderex.notification.AppNotification
+import com.twidere.twiderex.notification.AppNotificationManager
+import com.twidere.twiderex.notification.NotificationChannelSpec
+import com.twidere.twiderex.notification.notificationChannelId
+import com.twidere.twiderex.repository.AccountRepository
+import java.net.URI
+import java.util.UUID
+
+abstract class DirectMessageSendJob(
+ private val applicationContext: Context,
+ protected val cacheDatabase: CacheDatabase,
+ private val accountRepository: AccountRepository,
+ private val notificationManager: AppNotificationManager,
+ protected val fileResolver: FileResolver,
+) {
+ suspend fun execute(sendData: DirectMessageSendData, accountKey: MicroBlogKey) {
+ val accountDetails = accountKey.let {
+ accountRepository.findByAccountKey(accountKey = it)
+ }?.let {
+ accountRepository.getAccountDetails(it)
+ } ?: throw Error("can't find any account matches:$accountKey")
+ val notificationId = sendData.draftMessageKey.hashCode()
+ @Suppress("UNCHECKED_CAST")
+ val service = accountDetails.service as T
+
+ var draftEvent: DbDMEventWithAttachments? = null
+ try {
+ val images = sendData.images
+ draftEvent = getDraft(sendData, images, accountDetails) ?: throw IllegalArgumentException()
+ // val exifScrambler = ExifScrambler(context)
+ val mediaIds = arrayListOf()
+
+ images.forEach { uri ->
+ // val scramblerUri = exifScrambler.removeExifData(uri)
+ // TODO FIXME 2020/6/30 Twitter DM throws bad media error after remove exif data from images
+ //
+ val id = uploadImage(uri, uri, service)
+ id?.let { mediaIds.add(it) }
+ // exifScrambler.deleteCacheFile(scramblerUri)
+ }
+ val dbEvent = sendMessage(service, sendData, mediaIds)
+ updateDb(draftEvent, dbEvent)
+ } catch (e: Throwable) {
+ e.printStackTrace()
+ draftEvent?.let {
+ cacheDatabase.directMessageDao()
+ .insertAll(listOf(draftEvent.message.copy(sendStatus = DbDMEvent.SendStatus.FAILED)))
+ }
+ val builder = AppNotification
+ .Builder(
+ accountDetails.accountKey.notificationChannelId(
+ NotificationChannelSpec.ContentMessages.id
+ )
+ )
+ .setContentTitle(applicationContext.getString(R.string.common_alerts_failed_to_send_message_message))
+ .setContentText(sendData.text)
+ .setDeepLink(RootDeepLinksRoute.Conversation(sendData.conversationKey))
+ notificationManager.notify(notificationId, builder.build())
+ throw e
+ }
+ }
+
+ private suspend fun updateDb(draftEvent: DbDMEventWithAttachments?, dbEvent: DbDMEventWithAttachments) {
+ cacheDatabase.withTransaction {
+ draftEvent?.let {
+ cacheDatabase.directMessageDao().delete(
+ it.message
+ )
+ }
+ listOf(dbEvent).saveToDb(cacheDatabase)
+ }
+ }
+
+ private suspend fun getDraft(sendData: DirectMessageSendData, images: List, account: AccountDetails): DbDMEventWithAttachments? {
+ return cacheDatabase.withTransaction {
+ cacheDatabase.directMessageDao().findWithMessageKey(
+ account.accountKey,
+ sendData.conversationKey,
+ sendData.draftMessageKey
+ )?.also {
+ cacheDatabase.directMessageDao().insertAll(
+ listOf(it.message.copy(sendStatus = DbDMEvent.SendStatus.PENDING))
+ )
+ }
+ } ?: saveDraft(sendData, images, account)
+ }
+
+ private suspend fun saveDraft(sendData: DirectMessageSendData, images: List, account: AccountDetails): DbDMEventWithAttachments? {
+ return cacheDatabase.withTransaction {
+ val createTimeStamp = System.currentTimeMillis()
+ listOf(
+ DbDMEvent(
+ _id = UUID.randomUUID().toString(),
+ accountKey = account.accountKey,
+ sortId = createTimeStamp,
+ conversationKey = sendData.conversationKey,
+ messageId = sendData.draftMessageKey.id,
+ messageKey = sendData.draftMessageKey,
+ htmlText = autoLink(sendData.text ?: ""),
+ originText = sendData.text ?: "",
+ createdTimestamp = createTimeStamp,
+ messageType = "message_create",
+ senderAccountKey = account.accountKey,
+ recipientAccountKey = sendData.recipientUserKey,
+ sendStatus = DbDMEvent.SendStatus.PENDING
+ )
+ ).saveToDb(cacheDatabase)
+ cacheDatabase.mediaDao().insertAll(
+ images.mapIndexed { index, uri ->
+ val imageSize = getImageSize(URI.create(uri).path)
+ DbMedia(
+ _id = UUID.randomUUID().toString(),
+ belongToKey = sendData.draftMessageKey,
+ url = uri.toString(),
+ mediaUrl = uri.toString(),
+ previewUrl = uri.toString(),
+ type = getMediaType(uri),
+ width = imageSize[0],
+ height = imageSize[1],
+ altText = "",
+ order = index,
+ pageUrl = null,
+ )
+ }
+ )
+ cacheDatabase.directMessageDao().findWithMessageKey(
+ account.accountKey,
+ sendData.conversationKey,
+ sendData.draftMessageKey
+ )
+ }
+ }
+
+ private fun getMediaType(uri: String): MediaType {
+ val type = fileResolver.getMimeType(uri) ?: ""
+ return when {
+ type.startsWith("image") -> MediaType.photo
+ type.startsWith("video") -> MediaType.video
+ else -> MediaType.other
+ }
+ }
+
+ private fun getImageSize(path: String?): Array {
+ return path?.let {
+ val options = BitmapFactory.Options()
+ options.inJustDecodeBounds = true
+ BitmapFactory.decodeFile(it, options)
+ arrayOf(
+ options.outWidth.toLong(),
+ options.outHeight.toLong()
+ )
+ } ?: arrayOf(0, 0)
+ }
+
+ protected abstract suspend fun sendMessage(
+ service: T,
+ sendData: DirectMessageSendData,
+ mediaIds: ArrayList
+ ): DbDMEventWithAttachments
+
+ protected abstract suspend fun uploadImage(
+ originUri: String,
+ scramblerUri: String,
+ service: T
+ ): String?
+
+ protected abstract suspend fun autoLink(text: String): String
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/jobs/dm/TwitterDirectMessageSendJob.kt b/app/src/main/kotlin/com/twidere/twiderex/jobs/dm/TwitterDirectMessageSendJob.kt
new file mode 100644
index 000000000..4b1b4615a
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/jobs/dm/TwitterDirectMessageSendJob.kt
@@ -0,0 +1,90 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.jobs.dm
+
+import android.content.Context
+import com.twidere.services.microblog.LookupService
+import com.twidere.services.twitter.TwitterService
+import com.twidere.twiderex.db.CacheDatabase
+import com.twidere.twiderex.db.mapper.autolink
+import com.twidere.twiderex.db.mapper.toDbDirectMessage
+import com.twidere.twiderex.db.mapper.toDbUser
+import com.twidere.twiderex.db.model.DbDMEventWithAttachments
+import com.twidere.twiderex.db.model.DbUser
+import com.twidere.twiderex.kmp.FileResolver
+import com.twidere.twiderex.model.MicroBlogKey
+import com.twidere.twiderex.model.job.DirectMessageSendData
+import com.twidere.twiderex.notification.AppNotificationManager
+import com.twidere.twiderex.repository.AccountRepository
+
+class TwitterDirectMessageSendJob(
+ context: Context,
+ accountRepository: AccountRepository,
+ notificationManager: AppNotificationManager,
+ fileResolver: FileResolver,
+ cacheDatabase: CacheDatabase,
+) : DirectMessageSendJob(
+ context, cacheDatabase, accountRepository, notificationManager, fileResolver
+) {
+
+ override suspend fun sendMessage(
+ service: TwitterService,
+ sendData: DirectMessageSendData,
+ mediaIds: ArrayList
+ ): DbDMEventWithAttachments = service.sendDirectMessage(
+ recipientId = sendData.recipientUserKey.id,
+ text = sendData.text,
+ attachmentType = "media",
+ mediaId = mediaIds.firstOrNull()
+ )?.toDbDirectMessage(
+ accountKey = sendData.accountKey,
+ sender = lookUpUser(cacheDatabase, sendData.accountKey, service)
+ ) ?: throw Error()
+
+ private suspend fun lookUpUser(database: CacheDatabase, userKey: MicroBlogKey, service: TwitterService): DbUser {
+ return database.userDao().findWithUserKey(userKey) ?: let {
+ val user = (service as LookupService).lookupUser(userKey.id)
+ .toDbUser(userKey)
+ database.userDao().insertAll(listOf(user))
+ user
+ }
+ }
+
+ override suspend fun uploadImage(
+ originUri: String,
+ scramblerUri: String,
+ service: TwitterService
+ ): String? {
+ val type = fileResolver.getMimeType(originUri)
+ val size = fileResolver.getFileSize(originUri)
+ return fileResolver.openInputStream(scramblerUri)?.use {
+ service.uploadFile(
+ it,
+ type ?: "image/*",
+ size ?: it.available().toLong()
+ )
+ } ?: throw Error()
+ }
+
+ override suspend fun autoLink(text: String): String {
+ return autolink.autoLink(text)
+ }
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/jobs/draft/RemoveDraftJob.kt b/app/src/main/kotlin/com/twidere/twiderex/jobs/draft/RemoveDraftJob.kt
new file mode 100644
index 000000000..b2f675cd6
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/jobs/draft/RemoveDraftJob.kt
@@ -0,0 +1,31 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.jobs.draft
+
+import com.twidere.twiderex.repository.DraftRepository
+
+class RemoveDraftJob(
+ private val repository: DraftRepository,
+) {
+ suspend fun execute(draftId: String) {
+ repository.remove(draftId = draftId)
+ }
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/jobs/draft/SaveDraftJob.kt b/app/src/main/kotlin/com/twidere/twiderex/jobs/draft/SaveDraftJob.kt
new file mode 100644
index 000000000..8b6342178
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/jobs/draft/SaveDraftJob.kt
@@ -0,0 +1,50 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.jobs.draft
+
+import com.twidere.twiderex.model.job.ComposeData
+import com.twidere.twiderex.notification.InAppNotification
+import com.twidere.twiderex.repository.DraftRepository
+import com.twidere.twiderex.utils.notify
+
+class SaveDraftJob(
+ private val repository: DraftRepository,
+ private val inAppNotification: InAppNotification,
+) {
+ suspend fun execute(data: ComposeData) {
+ with(data) {
+ try {
+ repository.addOrUpgrade(
+ content,
+ images,
+ composeType = composeType,
+ statusKey = statusKey,
+ draftId = draftId,
+ excludedReplyUserIds = excludedReplyUserIds,
+ )
+ true
+ } catch (e: Throwable) {
+ e.notify(inAppNotification)
+ throw e
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/jobs/status/DeleteStatusJob.kt b/app/src/main/kotlin/com/twidere/twiderex/jobs/status/DeleteStatusJob.kt
new file mode 100644
index 000000000..3ffa66a87
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/jobs/status/DeleteStatusJob.kt
@@ -0,0 +1,54 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.jobs.status
+
+import com.twidere.services.microblog.StatusService
+import com.twidere.twiderex.model.MicroBlogKey
+import com.twidere.twiderex.notification.InAppNotification
+import com.twidere.twiderex.repository.AccountRepository
+import com.twidere.twiderex.repository.StatusRepository
+import com.twidere.twiderex.utils.notify
+
+class DeleteStatusJob(
+ private val accountRepository: AccountRepository,
+ private val statusRepository: StatusRepository,
+ private val inAppNotification: InAppNotification
+) {
+ suspend fun execute(
+ accountKey: MicroBlogKey,
+ statusKey: MicroBlogKey
+ ) {
+ val status = statusKey.let {
+ statusRepository.loadFromCache(it, accountKey = accountKey)
+ } ?: throw Error("Can't find any status matches:$statusKey")
+ val service = accountRepository.findByAccountKey(accountKey)?.let {
+ accountRepository.getAccountDetails(it)
+ }?.let {
+ it.service as? StatusService
+ } ?: throw Error()
+ try {
+ service.delete(status.statusId)
+ } catch (e: Throwable) {
+ e.notify(inAppNotification)
+ throw e
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/jobs/status/LikeStatusJob.kt b/app/src/main/kotlin/com/twidere/twiderex/jobs/status/LikeStatusJob.kt
new file mode 100644
index 000000000..0adb1dfa4
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/jobs/status/LikeStatusJob.kt
@@ -0,0 +1,67 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.jobs.status
+
+import com.twidere.services.microblog.StatusService
+import com.twidere.twiderex.db.mapper.toDbStatusWithReference
+import com.twidere.twiderex.model.MicroBlogKey
+import com.twidere.twiderex.model.job.StatusResult
+import com.twidere.twiderex.model.transform.toUi
+import com.twidere.twiderex.model.ui.UiStatus
+import com.twidere.twiderex.notification.InAppNotification
+import com.twidere.twiderex.repository.AccountRepository
+import com.twidere.twiderex.repository.StatusRepository
+
+class LikeStatusJob(
+ accountRepository: AccountRepository,
+ statusRepository: StatusRepository,
+ inAppNotification: InAppNotification,
+) : StatusJob(
+ accountRepository, statusRepository, inAppNotification
+) {
+ override suspend fun doWork(
+ accountKey: MicroBlogKey,
+ service: StatusService,
+ status: UiStatus
+ ): StatusResult {
+ val newStatus = service.like(status.statusId)
+ .toDbStatusWithReference(accountKey = accountKey)
+ .toUi(accountKey = accountKey).let {
+ it.retweet ?: it
+ }
+ return StatusResult(
+ statusKey = newStatus.statusKey,
+ accountKey = accountKey,
+ liked = true,
+ retweetCount = newStatus.metrics.retweet,
+ likeCount = newStatus.metrics.like,
+ )
+ }
+
+ override fun fallback(
+ accountKey: MicroBlogKey,
+ status: UiStatus,
+ ) = StatusResult(
+ accountKey = accountKey,
+ statusKey = status.statusKey,
+ liked = false,
+ )
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/jobs/status/MastodonVoteJob.kt b/app/src/main/kotlin/com/twidere/twiderex/jobs/status/MastodonVoteJob.kt
new file mode 100644
index 000000000..0bbd2f102
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/jobs/status/MastodonVoteJob.kt
@@ -0,0 +1,82 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.jobs.status
+
+import com.twidere.services.mastodon.MastodonService
+import com.twidere.services.mastodon.model.Poll
+import com.twidere.twiderex.db.model.DbMastodonStatusExtra
+import com.twidere.twiderex.model.MicroBlogKey
+import com.twidere.twiderex.model.enums.PlatformType
+import com.twidere.twiderex.notification.InAppNotification
+import com.twidere.twiderex.repository.AccountRepository
+import com.twidere.twiderex.repository.StatusRepository
+import com.twidere.twiderex.utils.fromJson
+import com.twidere.twiderex.utils.json
+import com.twidere.twiderex.utils.notify
+
+class MastodonVoteJob(
+ private val accountRepository: AccountRepository,
+ private val statusRepository: StatusRepository,
+ private val inAppNotification: InAppNotification,
+) {
+ suspend fun execute(votes: List, accountKey: MicroBlogKey, statusKey: MicroBlogKey) {
+ val status = statusRepository.loadFromCache(statusKey, accountKey = accountKey) ?: throw Error("Can't find any status matches:$statusKey")
+ if (status.poll == null || status.platformType != PlatformType.Mastodon) {
+ throw Error()
+ }
+ val service = accountRepository.findByAccountKey(accountKey)?.let {
+ accountRepository.getAccountDetails(it)
+ }?.let {
+ it.service as? MastodonService
+ } ?: throw Error()
+
+ val pollId = status.poll.id
+ var originPoll: Poll? = null
+ statusRepository.updateStatus(statusKey = status.statusKey) {
+ it.extra = it.extra.fromJson()
+ .let { extra ->
+ originPoll = extra.poll
+ extra.copy(
+ poll = extra.poll?.copy(
+ voted = true,
+ ownVotes = votes
+ )
+ )
+ }.json()
+ }
+ try {
+ val newPoll = service.vote(pollId, votes)
+ statusRepository.updateStatus(statusKey = status.statusKey) {
+ it.extra = it.extra.fromJson().copy(
+ poll = newPoll
+ ).json()
+ }
+ } catch (e: Throwable) {
+ statusRepository.updateStatus(statusKey = status.statusKey) {
+ it.extra = it.extra.fromJson().copy(
+ poll = originPoll
+ ).json()
+ }
+ e.notify(inAppNotification)
+ throw e
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/jobs/status/RetweetStatusJob.kt b/app/src/main/kotlin/com/twidere/twiderex/jobs/status/RetweetStatusJob.kt
new file mode 100644
index 000000000..fcc25f2dc
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/jobs/status/RetweetStatusJob.kt
@@ -0,0 +1,66 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.jobs.status
+
+import com.twidere.services.microblog.StatusService
+import com.twidere.twiderex.db.mapper.toDbStatusWithReference
+import com.twidere.twiderex.model.MicroBlogKey
+import com.twidere.twiderex.model.job.StatusResult
+import com.twidere.twiderex.model.transform.toUi
+import com.twidere.twiderex.model.ui.UiStatus
+import com.twidere.twiderex.notification.InAppNotification
+import com.twidere.twiderex.repository.AccountRepository
+import com.twidere.twiderex.repository.StatusRepository
+
+class RetweetStatusJob(
+ accountRepository: AccountRepository,
+ statusRepository: StatusRepository,
+ inAppNotification: InAppNotification,
+) : StatusJob(
+ accountRepository, statusRepository, inAppNotification
+) {
+ override suspend fun doWork(
+ accountKey: MicroBlogKey,
+ service: StatusService,
+ status: UiStatus
+ ): StatusResult {
+ val newStatus = service.retweet(status.statusId)
+ .toDbStatusWithReference(accountKey = accountKey)
+ .toUi(accountKey = accountKey).let {
+ it.retweet ?: it
+ }
+ return StatusResult(
+ statusKey = newStatus.statusKey,
+ accountKey = accountKey,
+ retweeted = true,
+ retweetCount = newStatus.metrics.retweet,
+ likeCount = newStatus.metrics.like,
+ )
+ }
+ override fun fallback(
+ accountKey: MicroBlogKey,
+ status: UiStatus,
+ ) = StatusResult(
+ accountKey = accountKey,
+ statusKey = status.statusKey,
+ retweeted = false,
+ )
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/jobs/status/StatusJob.kt b/app/src/main/kotlin/com/twidere/twiderex/jobs/status/StatusJob.kt
new file mode 100644
index 000000000..85ef159b6
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/jobs/status/StatusJob.kt
@@ -0,0 +1,96 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.jobs.status
+
+import com.twidere.services.microblog.StatusService
+import com.twidere.services.twitter.TwitterErrorCodes
+import com.twidere.services.twitter.model.exceptions.TwitterApiException
+import com.twidere.twiderex.model.MicroBlogKey
+import com.twidere.twiderex.model.job.StatusResult
+import com.twidere.twiderex.model.ui.UiStatus
+import com.twidere.twiderex.notification.InAppNotification
+import com.twidere.twiderex.repository.AccountRepository
+import com.twidere.twiderex.repository.StatusRepository
+import com.twidere.twiderex.utils.notify
+
+abstract class StatusJob(
+ private val accountRepository: AccountRepository,
+ private val statusRepository: StatusRepository,
+ private val inAppNotification: InAppNotification,
+) {
+ suspend fun execute(accountKey: MicroBlogKey, statusKey: MicroBlogKey): StatusResult {
+ val status = statusKey.let {
+ statusRepository.loadFromCache(it, accountKey = accountKey)
+ } ?: throw Error("can't find any status matches:$statusKey")
+ val service = accountRepository.findByAccountKey(accountKey)?.let {
+ accountRepository.getAccountDetails(it)
+ }?.let {
+ it.service as? StatusService
+ } ?: throw Error("account service is not StatusService")
+ return try {
+ return doWork(accountKey, service, status)
+ } catch (e: TwitterApiException) {
+ e.errors?.firstOrNull()?.let {
+ when (it.code) {
+ TwitterErrorCodes.AlreadyRetweeted -> {
+ StatusResult(
+ accountKey = accountKey,
+ statusKey = status.statusKey,
+ retweeted = true,
+ )
+ }
+ TwitterErrorCodes.AlreadyFavorited -> {
+ StatusResult(
+ accountKey = accountKey,
+ statusKey = status.statusKey,
+ liked = true,
+ )
+ }
+ else -> {
+ e.notify(inAppNotification)
+ fallback(accountKey, status)
+ }
+ }
+ } ?: run {
+ e.notify(inAppNotification)
+ fallback(accountKey, status)
+ }
+ } catch (e: Throwable) {
+ e.notify(inAppNotification)
+ fallback(accountKey, status)
+ }
+ }
+ protected abstract suspend fun doWork(
+ accountKey: MicroBlogKey,
+ service: StatusService,
+ status: UiStatus,
+ ): StatusResult
+
+ protected open fun fallback(
+ accountKey: MicroBlogKey,
+ status: UiStatus,
+ ) = StatusResult(
+ accountKey = accountKey,
+ statusKey = status.statusKey,
+ liked = status.liked,
+ retweeted = status.retweeted,
+ )
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/jobs/status/UnRetweetStatusJob.kt b/app/src/main/kotlin/com/twidere/twiderex/jobs/status/UnRetweetStatusJob.kt
new file mode 100644
index 000000000..d8f5e35e1
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/jobs/status/UnRetweetStatusJob.kt
@@ -0,0 +1,66 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.jobs.status
+
+import com.twidere.services.microblog.StatusService
+import com.twidere.twiderex.db.mapper.toDbStatusWithReference
+import com.twidere.twiderex.model.MicroBlogKey
+import com.twidere.twiderex.model.job.StatusResult
+import com.twidere.twiderex.model.transform.toUi
+import com.twidere.twiderex.model.ui.UiStatus
+import com.twidere.twiderex.notification.InAppNotification
+import com.twidere.twiderex.repository.AccountRepository
+import com.twidere.twiderex.repository.StatusRepository
+
+class UnRetweetStatusJob(
+ accountRepository: AccountRepository,
+ statusRepository: StatusRepository,
+ inAppNotification: InAppNotification,
+) : StatusJob(
+ accountRepository, statusRepository, inAppNotification
+) {
+ override suspend fun doWork(
+ accountKey: MicroBlogKey,
+ service: StatusService,
+ status: UiStatus
+ ): StatusResult {
+ val newStatus = service.unRetweet(status.statusId)
+ .toDbStatusWithReference(accountKey = accountKey)
+ .toUi(accountKey = accountKey).let {
+ it.retweet ?: it
+ }
+ return StatusResult(
+ statusKey = newStatus.statusKey,
+ accountKey = accountKey,
+ retweeted = false,
+ retweetCount = newStatus.metrics.retweet,
+ likeCount = newStatus.metrics.like,
+ )
+ }
+ override fun fallback(
+ accountKey: MicroBlogKey,
+ status: UiStatus,
+ ) = StatusResult(
+ accountKey = accountKey,
+ statusKey = status.statusKey,
+ retweeted = true,
+ )
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/jobs/status/UnlikeStatusJob.kt b/app/src/main/kotlin/com/twidere/twiderex/jobs/status/UnlikeStatusJob.kt
new file mode 100644
index 000000000..18835bd3e
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/jobs/status/UnlikeStatusJob.kt
@@ -0,0 +1,66 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.jobs.status
+
+import com.twidere.services.microblog.StatusService
+import com.twidere.twiderex.db.mapper.toDbStatusWithReference
+import com.twidere.twiderex.model.MicroBlogKey
+import com.twidere.twiderex.model.job.StatusResult
+import com.twidere.twiderex.model.transform.toUi
+import com.twidere.twiderex.model.ui.UiStatus
+import com.twidere.twiderex.notification.InAppNotification
+import com.twidere.twiderex.repository.AccountRepository
+import com.twidere.twiderex.repository.StatusRepository
+
+class UnlikeStatusJob(
+ accountRepository: AccountRepository,
+ statusRepository: StatusRepository,
+ inAppNotification: InAppNotification,
+) : StatusJob(
+ accountRepository, statusRepository, inAppNotification
+) {
+ override suspend fun doWork(
+ accountKey: MicroBlogKey,
+ service: StatusService,
+ status: UiStatus
+ ): StatusResult {
+ val newStatus = service.unlike(status.statusId)
+ .toDbStatusWithReference(accountKey = accountKey)
+ .toUi(accountKey = accountKey).let {
+ it.retweet ?: it
+ }
+ return StatusResult(
+ statusKey = newStatus.statusKey,
+ accountKey = accountKey,
+ liked = false,
+ retweetCount = newStatus.metrics.retweet,
+ likeCount = newStatus.metrics.like,
+ )
+ }
+ override fun fallback(
+ accountKey: MicroBlogKey,
+ status: UiStatus,
+ ) = StatusResult(
+ accountKey = accountKey,
+ statusKey = status.statusKey,
+ liked = true,
+ )
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/kmp/ExifScrambler.kt b/app/src/main/kotlin/com/twidere/twiderex/kmp/ExifScrambler.kt
new file mode 100644
index 000000000..6b68a250c
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/kmp/ExifScrambler.kt
@@ -0,0 +1,27 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.kmp
+
+interface ExifScrambler {
+ fun removeExifData(file: String, compress: Int = 100): String
+
+ fun deleteCacheFile(file: String)
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/kmp/FileResolver.kt b/app/src/main/kotlin/com/twidere/twiderex/kmp/FileResolver.kt
new file mode 100644
index 000000000..4df89a1ba
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/kmp/FileResolver.kt
@@ -0,0 +1,34 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.kmp
+
+import java.io.InputStream
+import java.io.OutputStream
+
+interface FileResolver {
+ fun getMimeType(file: String): String?
+
+ fun getFileSize(file: String): Long?
+
+ fun openInputStream(file: String): InputStream?
+
+ fun openOutputStream(file: String): OutputStream?
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/kmp/RemoteNavigator.kt b/app/src/main/kotlin/com/twidere/twiderex/kmp/RemoteNavigator.kt
new file mode 100644
index 000000000..9a6107c14
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/kmp/RemoteNavigator.kt
@@ -0,0 +1,29 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.kmp
+
+interface RemoteNavigator {
+ fun openDeepLink(deeplink: String, fromBackground: Boolean = false)
+
+ fun shareMedia(filePath: String, mimeType: String, fromBackground: Boolean = false)
+
+ fun shareText(content: String, fromBackground: Boolean = false)
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/utils/ExifScrambler.kt b/app/src/main/kotlin/com/twidere/twiderex/kmp/android/AndroidExifScrambler.kt
similarity index 88%
rename from app/src/main/kotlin/com/twidere/twiderex/utils/ExifScrambler.kt
rename to app/src/main/kotlin/com/twidere/twiderex/kmp/android/AndroidExifScrambler.kt
index 336e1856c..fb9112bca 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/utils/ExifScrambler.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/kmp/android/AndroidExifScrambler.kt
@@ -18,7 +18,7 @@
* You should have received a copy of the GNU General Public License
* along with Twidere X. If not, see .
*/
-package com.twidere.twiderex.utils
+package com.twidere.twiderex.kmp.android
import android.content.Context
import android.graphics.Bitmap
@@ -26,19 +26,21 @@ import android.graphics.BitmapFactory
import android.net.Uri
import androidx.core.net.toUri
import androidx.exifinterface.media.ExifInterface
+import com.twidere.twiderex.kmp.ExifScrambler
import java.io.File
import java.util.UUID
-class ExifScrambler(private val context: Context) {
- fun removeExifData(uri: Uri, compress: Int = 100): Uri {
+class AndroidExifScrambler(private val context: Context) : ExifScrambler {
+ override fun removeExifData(file: String, compress: Int): String {
// first get input stream
+ val uri = Uri.parse(file)
val contentResolver = context.contentResolver
contentResolver.openInputStream(uri)?.use { input ->
// decode to bitmap because bitmap won't store exif meta data
val bitmap = try {
BitmapFactory.decodeStream(input)
} catch (oom: OutOfMemoryError) {
- return uri
+ return file
}
// create an cache image
val mimeType = contentResolver.getType(uri) ?: ""
@@ -74,13 +76,13 @@ class ExifScrambler(private val context: Context) {
}
}
}
- return imageCache.toUri()
+ return imageCache.toUri().toString()
}
- return uri
+ return uri.toString()
}
- fun deleteCacheFile(uri: Uri) {
- uri.path?.let {
+ override fun deleteCacheFile(file: String) {
+ Uri.parse(file).path?.let {
File(it)
}?.apply {
if (exists()) delete()
diff --git a/app/src/main/kotlin/com/twidere/twiderex/kmp/android/AndroidFileResolver.kt b/app/src/main/kotlin/com/twidere/twiderex/kmp/android/AndroidFileResolver.kt
new file mode 100644
index 000000000..0cfc7e6a1
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/kmp/android/AndroidFileResolver.kt
@@ -0,0 +1,45 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.kmp.android
+
+import android.content.ContentResolver
+import android.net.Uri
+import com.twidere.twiderex.kmp.FileResolver
+import java.io.InputStream
+import java.io.OutputStream
+
+class AndroidFileResolver(private val contentResolver: ContentResolver) : FileResolver {
+ override fun getMimeType(file: String): String? {
+ return contentResolver.getType(Uri.parse(file))
+ }
+
+ override fun getFileSize(file: String): Long? {
+ return contentResolver.openFileDescriptor(Uri.parse(file), "r")?.statSize
+ }
+
+ override fun openInputStream(file: String): InputStream? {
+ return contentResolver.openInputStream(Uri.parse(file))
+ }
+
+ override fun openOutputStream(file: String): OutputStream? {
+ return contentResolver.openOutputStream(Uri.parse(file))
+ }
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/kmp/android/AndroidNotificationManager.kt b/app/src/main/kotlin/com/twidere/twiderex/kmp/android/AndroidNotificationManager.kt
new file mode 100644
index 000000000..70a007fc1
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/kmp/android/AndroidNotificationManager.kt
@@ -0,0 +1,93 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.kmp.android
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import com.twidere.twiderex.R
+import com.twidere.twiderex.notification.AppNotification
+import com.twidere.twiderex.notification.AppNotificationManager
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import java.util.concurrent.TimeUnit
+import kotlin.time.ExperimentalTime
+
+class AndroidNotificationManager(
+ private val context: Context,
+ private val notificationManagerCompat: NotificationManagerCompat
+) : AppNotificationManager {
+ val scope = MainScope()
+ override fun notify(notificationId: Int, appNotification: AppNotification) {
+ val builder = NotificationCompat.Builder(
+ context,
+ appNotification.channelId
+ ).setSmallIcon(R.drawable.ic_notification)
+ .setCategory(NotificationCompat.CATEGORY_SOCIAL)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setAutoCancel(true)
+ .setContentTitle(appNotification.title)
+ .setOngoing(appNotification.ongoing)
+ .setSilent(appNotification.silent)
+ .setProgress(appNotification.progressMax, appNotification.progress, appNotification.progressIndeterminate)
+ appNotification.content?.let {
+ builder.setContentText(it)
+ .setStyle(NotificationCompat.BigTextStyle().bigText(it))
+ }
+ appNotification.deepLink?.let {
+ builder.setContentIntent(
+ PendingIntent.getActivity(
+ context,
+ 0,
+ Intent(Intent.ACTION_VIEW, Uri.parse(it)),
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ PendingIntent.FLAG_IMMUTABLE
+ } else {
+ PendingIntent.FLAG_UPDATE_CURRENT
+ },
+ )
+ )
+ }
+ appNotification.largeIcon?.let {
+ builder.setLargeIcon(it)
+ }
+ notificationManagerCompat.notify(notificationId, builder.build())
+ }
+
+ @OptIn(ExperimentalTime::class)
+ override fun notifyTransient(
+ notificationId: Int,
+ appNotification: AppNotification,
+ duration: Long,
+ durationTimeUnit: TimeUnit
+ ) {
+ notify(notificationId, appNotification)
+ scope.launch {
+ delay(durationTimeUnit.toMillis(duration))
+ notificationManagerCompat.cancel(notificationId)
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/kmp/android/AndroidRemoteNavigator.kt b/app/src/main/kotlin/com/twidere/twiderex/kmp/android/AndroidRemoteNavigator.kt
new file mode 100644
index 000000000..77bf60422
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/kmp/android/AndroidRemoteNavigator.kt
@@ -0,0 +1,56 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.kmp.android
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import com.twidere.twiderex.extensions.shareMedia
+import com.twidere.twiderex.extensions.shareText
+import com.twidere.twiderex.kmp.RemoteNavigator
+
+class AndroidRemoteNavigator(private val context: Context) : RemoteNavigator {
+ override fun openDeepLink(deeplink: String, fromBackground: Boolean) {
+ context.startActivity(
+ Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse(deeplink)
+ ).apply {
+ if (fromBackground) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ )
+ }
+
+ override fun shareMedia(filePath: String, mimeType: String, fromBackground: Boolean) {
+ context.shareMedia(
+ uri = Uri.parse(filePath),
+ mimeType = mimeType,
+ fromOutsideOfActivity = fromBackground
+ )
+ }
+
+ override fun shareText(content: String, fromBackground: Boolean) {
+ context.shareText(
+ content = content,
+ fromOutsideOfActivity = fromBackground
+ )
+ }
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/AccountDetails.kt b/app/src/main/kotlin/com/twidere/twiderex/model/AccountDetails.kt
index d8791bd20..e2f1c28b4 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/model/AccountDetails.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/AccountDetails.kt
@@ -20,36 +20,30 @@
*/
package com.twidere.twiderex.model
-import android.accounts.Account
-import com.twidere.services.mastodon.MastodonService
-import com.twidere.services.microblog.MicroBlogService
-import com.twidere.services.twitter.TwitterService
-import com.twidere.twiderex.model.adapter.AndroidAccountSerializer
+import com.twidere.twiderex.http.TwidereServiceFactory
import com.twidere.twiderex.model.cred.BasicCredentials
import com.twidere.twiderex.model.cred.Credentials
import com.twidere.twiderex.model.cred.CredentialsType
import com.twidere.twiderex.model.cred.EmptyCredentials
import com.twidere.twiderex.model.cred.OAuth2Credentials
import com.twidere.twiderex.model.cred.OAuthCredentials
+import com.twidere.twiderex.model.enums.ListType
+import com.twidere.twiderex.model.enums.PlatformType
import com.twidere.twiderex.model.ui.UiUser
+import com.twidere.twiderex.model.ui.UserMetrics
import com.twidere.twiderex.utils.fromJson
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
-@Serializable
data class AccountDetails(
- @Serializable(with = AndroidAccountSerializer::class)
- val account: Account,
+ val account: TwidereAccount,
val type: PlatformType,
// Note that UserKey that being used in AccountDetails is idStr@domain, not screenName@domain
val accountKey: MicroBlogKey,
val credentials_type: CredentialsType,
- @SerialName("credentials")
var credentials_json: String,
- @SerialName("extras")
val extras_json: String,
var user: AmUser,
var lastActive: Long,
+ val preferences: AccountPreferences,
) {
val credentials: Credentials
get() = when (credentials_type) {
@@ -60,29 +54,12 @@ data class AccountDetails(
CredentialsType.OAuth2 -> credentials_json.fromJson()
}
- val service by lazy {
- when (type) {
- PlatformType.Twitter -> {
- credentials.let {
- it as OAuthCredentials
- }.let {
- TwitterService(
- consumer_key = it.consumer_key,
- consumer_secret = it.consumer_secret,
- access_token = it.access_token,
- access_token_secret = it.access_token_secret,
- )
- }
- }
- PlatformType.StatusNet -> TODO()
- PlatformType.Fanfou -> TODO()
- PlatformType.Mastodon ->
- credentials.let {
- it as OAuth2Credentials
- }.let {
- MastodonService(accountKey.host, it.access_token)
- }
- }
+ val service by lazy {
+ TwidereServiceFactory.createApiService(
+ type = type,
+ credentials = credentials,
+ host = accountKey.host
+ )
}
val listType: ListType
@@ -93,6 +70,8 @@ data class AccountDetails(
PlatformType.Mastodon -> ListType.Owned
}
+ val supportDirectMessage = type == PlatformType.Twitter
+
fun toUi() = with(user) {
UiUser(
id = userId,
@@ -100,9 +79,12 @@ data class AccountDetails(
screenName = screenName,
profileImage = profileImage,
profileBackgroundImage = profileBackgroundImage,
- followersCount = followersCount,
- friendsCount = friendsCount,
- listedCount = listedCount,
+ metrics = UserMetrics(
+ fans = followersCount,
+ follow = friendsCount,
+ listed = listedCount,
+ status = 0
+ ),
rawDesc = desc,
htmlDesc = desc,
website = website,
@@ -112,7 +94,6 @@ data class AccountDetails(
userKey = userKey,
platformType = type,
acct = userKey.copy(id = screenName),
- statusesCount = 0
)
}
}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/AccountPreferences.kt b/app/src/main/kotlin/com/twidere/twiderex/model/AccountPreferences.kt
index c737432e0..c1e7b85a9 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/model/AccountPreferences.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/AccountPreferences.kt
@@ -26,20 +26,39 @@ import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStoreFile
+import com.twidere.twiderex.scenes.home.HomeMenus
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.map
class AccountPreferences(
private val dataStore: DataStore,
+ private val scope: CoroutineScope,
) {
private val isNotificationEnabledKey = booleanPreferencesKey("isNotificationEnabled")
val isNotificationEnabled
get() = dataStore.data.map { preferences ->
preferences[isNotificationEnabledKey] ?: true
}
+ val homeMenuOrder
+ get() = dataStore.data.map { preferences ->
+ if (!preferences.contains(homeMenuOrderKey) || !preferences.contains(visibleHomeMenuKey)) {
+ HomeMenus.values().map { it to it.showDefault }
+ } else {
+ val order = preferences[homeMenuOrderKey].orEmpty()
+ .split(",")
+ .withIndex()
+ .associate { HomeMenus.valueOf(it.value) to it.index }
+ val visible = preferences[visibleHomeMenuKey].orEmpty().split(",")
+ HomeMenus.values().sortedBy {
+ order[it]
+ }.map { it to visible.contains(it.name) }
+ }
+ }
suspend fun setIsNotificationEnabled(value: Boolean) {
dataStore.edit {
@@ -47,6 +66,25 @@ class AccountPreferences(
}
}
+ fun close() {
+ // cancel scope will remove file from activeFiles in Datastore
+ // prevent crashes caused by multiple DataStores active for the same file
+ scope.cancel()
+ }
+
+ private val homeMenuOrderKey = stringPreferencesKey("homeMenuOrder")
+ private val visibleHomeMenuKey = stringPreferencesKey("visibleHomeMenu")
+ suspend fun setHomeMenuOrder(
+ data: List>,
+ ) {
+ dataStore.edit {
+ it[visibleHomeMenuKey] = data.filter { it.second }.joinToString(",") { it.first.name }
+ }
+ dataStore.edit {
+ it[homeMenuOrderKey] = data.joinToString(",") { it.first.name }
+ }
+ }
+
class Factory(
private val context: Context,
) {
@@ -55,14 +93,18 @@ class AccountPreferences(
private fun createAccountPreferences(
context: Context,
accountKey: MicroBlogKey,
- ) = AccountPreferences(
- dataStore = PreferenceDataStoreFactory.create(
- corruptionHandler = null,
- migrations = listOf(),
- scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
- ) {
- context.applicationContext.preferencesDataStoreFile(accountKey.toString())
- },
- )
+ ): AccountPreferences {
+ val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
+ return AccountPreferences(
+ dataStore = PreferenceDataStoreFactory.create(
+ corruptionHandler = null,
+ migrations = listOf(),
+ scope = scope
+ ) {
+ context.applicationContext.preferencesDataStoreFile(accountKey.toString())
+ },
+ scope = scope
+ )
+ }
}
}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/AmUser.kt b/app/src/main/kotlin/com/twidere/twiderex/model/AmUser.kt
index f379fd64f..2849f6718 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/model/AmUser.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/AmUser.kt
@@ -20,7 +20,6 @@
*/
package com.twidere.twiderex.model
-import com.twidere.twiderex.db.model.DbUser
import kotlinx.serialization.Serializable
@Serializable
@@ -40,21 +39,3 @@ data class AmUser(
val verified: Boolean,
val isProtected: Boolean,
)
-
-fun DbUser.toAmUser() =
- AmUser(
- userId = userId,
- name = name,
- userKey = userKey,
- screenName = screenName,
- profileImage = profileImage,
- profileBackgroundImage = profileBackgroundImage,
- followersCount = followersCount,
- friendsCount = friendsCount,
- listedCount = listedCount,
- desc = rawDesc,
- website = website,
- location = location,
- verified = verified,
- isProtected = isProtected,
- )
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/DirectMessageDeleteData.kt b/app/src/main/kotlin/com/twidere/twiderex/model/DirectMessageDeleteData.kt
deleted file mode 100644
index bdd2787c2..000000000
--- a/app/src/main/kotlin/com/twidere/twiderex/model/DirectMessageDeleteData.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Twidere X
- *
- * Copyright (C) 2020-2021 Tlaster
- *
- * This file is part of Twidere X.
- *
- * Twidere X is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * Twidere X is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Twidere X. If not, see .
- */
-package com.twidere.twiderex.model
-
-import androidx.work.Data
-import androidx.work.workDataOf
-
-data class DirectMessageDeleteData(
- val messageId: String,
- val conversationKey: MicroBlogKey,
- val messageKey: MicroBlogKey,
- val accountKey: MicroBlogKey,
-)
-
-fun DirectMessageDeleteData.toWorkData() = workDataOf(
- "accountKey" to accountKey.toString(),
- "conversationKey" to conversationKey.toString(),
- "messageKey" to messageKey.toString(),
- "messageId" to messageId,
-)
-
-fun Data.toDirectMessageDeleteData() = DirectMessageDeleteData(
- messageId = getString("messageId") ?: "",
- accountKey = MicroBlogKey.valueOf(getString("accountKey") ?: ""),
- conversationKey = MicroBlogKey.valueOf(getString("conversationKey") ?: ""),
- messageKey = MicroBlogKey.valueOf(getString("messageKey") ?: ""),
-)
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/DirectMessageSendData.kt b/app/src/main/kotlin/com/twidere/twiderex/model/DirectMessageSendData.kt
deleted file mode 100644
index d20682539..000000000
--- a/app/src/main/kotlin/com/twidere/twiderex/model/DirectMessageSendData.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Twidere X
- *
- * Copyright (C) 2020-2021 Tlaster
- *
- * This file is part of Twidere X.
- *
- * Twidere X is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * Twidere X is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Twidere X. If not, see .
- */
-package com.twidere.twiderex.model
-
-import androidx.work.Data
-import androidx.work.workDataOf
-
-data class DirectMessageSendData(
- val text: String?,
- val images: List,
- val recipientUserKey: MicroBlogKey,
- val conversationKey: MicroBlogKey,
- val accountKey: MicroBlogKey,
- val draftMessageKey: MicroBlogKey,
-)
-
-fun DirectMessageSendData.toWorkData() = workDataOf(
- "text" to text,
- "images" to images.toTypedArray(),
- "recipientUserKey" to recipientUserKey.toString(),
- "dratMessageKey" to draftMessageKey.toString(),
- "conversationKey" to conversationKey.toString(),
- "accountKey" to accountKey.toString(),
-)
-
-fun Data.toDirectMessageSendData() = DirectMessageSendData(
- text = getString("text") ?: "",
- images = getStringArray("images")?.toList() ?: emptyList(),
- recipientUserKey = MicroBlogKey.valueOf(getString("recipientUserKey") ?: ""),
- draftMessageKey = MicroBlogKey.valueOf(getString("dratMessageKey") ?: ""),
- conversationKey = MicroBlogKey.valueOf(getString("conversationKey") ?: ""),
- accountKey = MicroBlogKey.valueOf(getString("accountKey") ?: ""),
-)
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/JsonAccount.kt b/app/src/main/kotlin/com/twidere/twiderex/model/TwidereAccount.kt
similarity index 97%
rename from app/src/main/kotlin/com/twidere/twiderex/model/JsonAccount.kt
rename to app/src/main/kotlin/com/twidere/twiderex/model/TwidereAccount.kt
index 2cacb04d9..ba635a28c 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/model/JsonAccount.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/TwidereAccount.kt
@@ -23,7 +23,7 @@ package com.twidere.twiderex.model
import kotlinx.serialization.Serializable
@Serializable
-data class JsonAccount(
+data class TwidereAccount(
val name: String,
val type: String,
)
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/adapter/AndroidAccountAdapter.kt b/app/src/main/kotlin/com/twidere/twiderex/model/adapter/AndroidAccountAdapter.kt
deleted file mode 100644
index 58f88b02a..000000000
--- a/app/src/main/kotlin/com/twidere/twiderex/model/adapter/AndroidAccountAdapter.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Twidere X
- *
- * Copyright (C) 2020-2021 Tlaster
- *
- * This file is part of Twidere X.
- *
- * Twidere X is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * Twidere X is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Twidere X. If not, see .
- */
-package com.twidere.twiderex.model.adapter
-
-import android.accounts.Account
-import com.twidere.twiderex.model.JsonAccount
-import kotlinx.serialization.KSerializer
-import kotlinx.serialization.descriptors.PrimitiveKind
-import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
-import kotlinx.serialization.descriptors.SerialDescriptor
-import kotlinx.serialization.encoding.Decoder
-import kotlinx.serialization.encoding.Encoder
-
-object AndroidAccountSerializer : KSerializer {
- override val descriptor: SerialDescriptor
- get() = PrimitiveSerialDescriptor("Account", PrimitiveKind.STRING)
-
- override fun deserialize(decoder: Decoder): Account {
- return decoder.decodeSerializableValue(JsonAccount.serializer()).let {
- Account(it.name, it.type)
- }
- }
-
- override fun serialize(encoder: Encoder, value: Account) {
- encoder.encodeSerializableValue(JsonAccount.serializer(), JsonAccount(value.name, value.type))
- }
-}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/ListType.kt b/app/src/main/kotlin/com/twidere/twiderex/model/enums/ListType.kt
similarity index 95%
rename from app/src/main/kotlin/com/twidere/twiderex/model/ListType.kt
rename to app/src/main/kotlin/com/twidere/twiderex/model/enums/ListType.kt
index 39385b66e..5199dfb82 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/model/ListType.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/enums/ListType.kt
@@ -18,7 +18,7 @@
* You should have received a copy of the GNU General Public License
* along with Twidere X. If not, see .
*/
-package com.twidere.twiderex.model
+package com.twidere.twiderex.model.enums
enum class ListType {
All, // both owned and subscribed
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/MastodonStatusType.kt b/app/src/main/kotlin/com/twidere/twiderex/model/enums/Mastodon.kt
similarity index 88%
rename from app/src/main/kotlin/com/twidere/twiderex/model/MastodonStatusType.kt
rename to app/src/main/kotlin/com/twidere/twiderex/model/enums/Mastodon.kt
index dd63133fd..0741f8c5d 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/model/MastodonStatusType.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/enums/Mastodon.kt
@@ -18,7 +18,7 @@
* You should have received a copy of the GNU General Public License
* along with Twidere X. If not, see .
*/
-package com.twidere.twiderex.model
+package com.twidere.twiderex.model.enums
import kotlinx.serialization.Serializable
@@ -33,3 +33,11 @@ enum class MastodonStatusType {
NotificationPoll,
NotificationStatus,
}
+
+@Serializable
+enum class MastodonVisibility {
+ Public,
+ Unlisted,
+ Private,
+ Direct;
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/MediaType.kt b/app/src/main/kotlin/com/twidere/twiderex/model/enums/MediaType.kt
similarity index 95%
rename from app/src/main/kotlin/com/twidere/twiderex/model/MediaType.kt
rename to app/src/main/kotlin/com/twidere/twiderex/model/enums/MediaType.kt
index b47cce3bc..27315e2dc 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/model/MediaType.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/enums/MediaType.kt
@@ -18,7 +18,7 @@
* You should have received a copy of the GNU General Public License
* along with Twidere X. If not, see .
*/
-package com.twidere.twiderex.model
+package com.twidere.twiderex.model.enums
enum class MediaType {
photo,
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/PlatformType.kt b/app/src/main/kotlin/com/twidere/twiderex/model/enums/PlatformType.kt
similarity index 95%
rename from app/src/main/kotlin/com/twidere/twiderex/model/PlatformType.kt
rename to app/src/main/kotlin/com/twidere/twiderex/model/enums/PlatformType.kt
index a17bb1d19..fedc795ba 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/model/PlatformType.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/enums/PlatformType.kt
@@ -18,7 +18,7 @@
* You should have received a copy of the GNU General Public License
* along with Twidere X. If not, see .
*/
-package com.twidere.twiderex.model
+package com.twidere.twiderex.model.enums
enum class PlatformType {
Twitter,
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/enums/ReferenceType.kt b/app/src/main/kotlin/com/twidere/twiderex/model/enums/ReferenceType.kt
new file mode 100644
index 000000000..8e10b05d3
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/enums/ReferenceType.kt
@@ -0,0 +1,31 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.model.enums
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+enum class ReferenceType {
+ Retweet,
+ Reply,
+ Quote,
+ MastodonNotification,
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/enums/Twitter.kt b/app/src/main/kotlin/com/twidere/twiderex/model/enums/Twitter.kt
new file mode 100644
index 000000000..dca216f91
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/enums/Twitter.kt
@@ -0,0 +1,30 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.model.enums
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+enum class TwitterReplySettings {
+ Everyone,
+ MentionedUsers,
+ FollowingUsers,
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/job/ComposeData.kt b/app/src/main/kotlin/com/twidere/twiderex/model/job/ComposeData.kt
new file mode 100644
index 000000000..9b5fd745a
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/job/ComposeData.kt
@@ -0,0 +1,45 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.model.job
+
+import com.twidere.twiderex.model.MicroBlogKey
+import com.twidere.twiderex.model.enums.MastodonVisibility
+import com.twidere.twiderex.viewmodel.compose.ComposeType
+import com.twidere.twiderex.viewmodel.compose.VoteExpired
+import java.util.UUID
+
+data class ComposeData(
+ val content: String,
+ val images: List,
+ val composeType: ComposeType,
+ val statusKey: MicroBlogKey? = null,
+ val lat: Double? = null,
+ val long: Double? = null,
+ val draftId: String = UUID.randomUUID().toString(),
+ val excludedReplyUserIds: List? = null,
+ val voteOptions: List? = null,
+ val voteExpired: VoteExpired? = null,
+ val voteMultiple: Boolean? = null,
+ val visibility: MastodonVisibility? = null,
+ val isSensitive: Boolean? = null,
+ val contentWarningText: String? = null,
+ val isThreadMode: Boolean = false
+)
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/job/DirectMessageDeleteData.kt b/app/src/main/kotlin/com/twidere/twiderex/model/job/DirectMessageDeleteData.kt
new file mode 100644
index 000000000..1a7c22a6e
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/job/DirectMessageDeleteData.kt
@@ -0,0 +1,30 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.model.job
+
+import com.twidere.twiderex.model.MicroBlogKey
+
+data class DirectMessageDeleteData(
+ val messageId: String,
+ val conversationKey: MicroBlogKey,
+ val messageKey: MicroBlogKey,
+ val accountKey: MicroBlogKey,
+)
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/job/DirectMessageSendData.kt b/app/src/main/kotlin/com/twidere/twiderex/model/job/DirectMessageSendData.kt
new file mode 100644
index 000000000..a2f36d5bf
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/job/DirectMessageSendData.kt
@@ -0,0 +1,32 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.model.job
+
+import com.twidere.twiderex.model.MicroBlogKey
+
+data class DirectMessageSendData(
+ val text: String?,
+ val images: List,
+ val recipientUserKey: MicroBlogKey,
+ val conversationKey: MicroBlogKey,
+ val accountKey: MicroBlogKey,
+ val draftMessageKey: MicroBlogKey,
+)
diff --git a/app/src/main/kotlin/com/twidere/twiderex/worker/status/StatusResult.kt b/app/src/main/kotlin/com/twidere/twiderex/model/job/StatusResult.kt
similarity index 75%
rename from app/src/main/kotlin/com/twidere/twiderex/worker/status/StatusResult.kt
rename to app/src/main/kotlin/com/twidere/twiderex/model/job/StatusResult.kt
index a7e9b267f..f27c2f7e5 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/worker/status/StatusResult.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/job/StatusResult.kt
@@ -18,9 +18,8 @@
* You should have received a copy of the GNU General Public License
* along with Twidere X. If not, see .
*/
-package com.twidere.twiderex.worker.status
+package com.twidere.twiderex.model.job
-import androidx.work.workDataOf
import com.twidere.twiderex.model.MicroBlogKey
data class StatusResult(
@@ -30,13 +29,4 @@ data class StatusResult(
val liked: Boolean? = null,
val retweetCount: Long? = null,
val likeCount: Long? = null,
-) {
- fun toWorkData() = workDataOf(
- "statusKey" to statusKey.toString(),
- "accountKey" to accountKey.toString(),
- "liked" to liked,
- "retweeted" to retweeted,
- "retweetCount" to retweetCount,
- "likeCount" to likeCount,
- )
-}
+)
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/transform/AccountTransform.kt b/app/src/main/kotlin/com/twidere/twiderex/model/transform/AccountTransform.kt
new file mode 100644
index 000000000..8977eef5e
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/transform/AccountTransform.kt
@@ -0,0 +1,33 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.model.transform
+
+import android.accounts.Account
+import com.twidere.twiderex.model.TwidereAccount
+
+fun Account.toTwidere() = TwidereAccount(
+ name = name,
+ type = type
+)
+
+fun TwidereAccount.toAndroid() = Account(
+ name, type
+)
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/transform/DmConversationTransform.kt b/app/src/main/kotlin/com/twidere/twiderex/model/transform/DmConversationTransform.kt
new file mode 100644
index 000000000..b851f1cc1
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/transform/DmConversationTransform.kt
@@ -0,0 +1,62 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.model.transform
+
+import com.twidere.twiderex.db.model.DbDMConversation
+import com.twidere.twiderex.db.model.DbDMEventWithAttachments
+import com.twidere.twiderex.db.model.DbDirectMessageConversationWithMessage
+import com.twidere.twiderex.model.ui.UiDMConversation
+import com.twidere.twiderex.model.ui.UiDMConversationWithLatestMessage
+import com.twidere.twiderex.model.ui.UiDMEvent
+
+fun DbDMConversation.toUi() = UiDMConversation(
+ accountKey = accountKey,
+ conversationId = conversationId,
+ conversationKey = conversationKey,
+ conversationAvatar = conversationAvatar,
+ conversationName = conversationName,
+ conversationSubName = conversationSubName,
+ conversationType = conversationType,
+ recipientKey = recipientKey,
+)
+
+fun DbDirectMessageConversationWithMessage.toUi() = UiDMConversationWithLatestMessage(
+ conversation = conversation.toUi(),
+ latestMessage = latestMessage.toUi()
+)
+
+fun DbDMEventWithAttachments.toUi() = UiDMEvent(
+ accountKey = message.accountKey,
+ sortId = message.sortId,
+ conversationKey = message.conversationKey,
+ messageId = message.messageId,
+ messageKey = message.messageKey,
+ htmlText = message.htmlText,
+ originText = message.originText,
+ createdTimestamp = message.createdTimestamp,
+ messageType = message.messageType,
+ senderAccountKey = message.senderAccountKey,
+ recipientAccountKey = message.recipientAccountKey,
+ sendStatus = message.sendStatus,
+ media = media.toUi(),
+ urlEntity = urlEntity.toUi(),
+ sender = sender.toUi()
+)
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/transform/EmojiTransform.kt b/app/src/main/kotlin/com/twidere/twiderex/model/transform/EmojiTransform.kt
new file mode 100644
index 000000000..c886ebcf1
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/transform/EmojiTransform.kt
@@ -0,0 +1,40 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.model.transform
+
+import com.twidere.services.mastodon.model.Emoji
+import com.twidere.twiderex.model.ui.UiEmoji
+import com.twidere.twiderex.model.ui.UiEmojiCategory
+
+fun List.toUi(): List = groupBy({ it.category }, { it }).map {
+ UiEmojiCategory(
+ if (it.key.isNullOrEmpty()) null else it.key,
+ it.value.map { emoji ->
+ UiEmoji(
+ shortcode = emoji.shortcode,
+ url = emoji.url,
+ staticURL = emoji.staticURL,
+ visibleInPicker = emoji.visibleInPicker,
+ category = emoji.category
+ )
+ }
+ )
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/transform/ListTransform.kt b/app/src/main/kotlin/com/twidere/twiderex/model/transform/ListTransform.kt
new file mode 100644
index 000000000..7ff1f131f
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/transform/ListTransform.kt
@@ -0,0 +1,38 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.model.transform
+
+import com.twidere.twiderex.db.model.DbList
+import com.twidere.twiderex.model.ui.UiList
+
+fun DbList.toUi() =
+ UiList(
+ id = listId,
+ ownerId = ownerId,
+ listKey = listKey,
+ accountKey = accountKey,
+ title = title,
+ descriptions = description,
+ mode = mode,
+ replyPolicy = replyPolicy,
+ isFollowed = isFollowed,
+ allowToSubscribe = allowToSubscribe,
+ )
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/transform/MediaTransform.kt b/app/src/main/kotlin/com/twidere/twiderex/model/transform/MediaTransform.kt
new file mode 100644
index 000000000..acae8388e
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/transform/MediaTransform.kt
@@ -0,0 +1,37 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.model.transform
+
+import com.twidere.twiderex.db.model.DbMedia
+import com.twidere.twiderex.model.ui.UiMedia
+
+fun List.toUi() = sortedBy { it.order }.map {
+ UiMedia(
+ url = it.url,
+ mediaUrl = it.mediaUrl,
+ previewUrl = it.previewUrl,
+ type = it.type,
+ width = it.width,
+ height = it.height,
+ pageUrl = it.pageUrl,
+ altText = it.altText,
+ )
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/transform/StatusTransform.kt b/app/src/main/kotlin/com/twidere/twiderex/model/transform/StatusTransform.kt
new file mode 100644
index 000000000..e1cfc0691
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/transform/StatusTransform.kt
@@ -0,0 +1,203 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.model.transform
+
+import com.twidere.services.mastodon.model.Mention
+import com.twidere.services.mastodon.model.Poll
+import com.twidere.twiderex.db.model.DbMastodonStatusExtra
+import com.twidere.twiderex.db.model.DbPagingTimelineWithStatus
+import com.twidere.twiderex.db.model.DbPreviewCard
+import com.twidere.twiderex.db.model.DbStatusReaction
+import com.twidere.twiderex.db.model.DbStatusV2
+import com.twidere.twiderex.db.model.DbStatusWithMediaAndUser
+import com.twidere.twiderex.db.model.DbStatusWithReference
+import com.twidere.twiderex.db.model.DbTwitterStatusExtra
+import com.twidere.twiderex.model.MicroBlogKey
+import com.twidere.twiderex.model.enums.PlatformType
+import com.twidere.twiderex.model.enums.ReferenceType
+import com.twidere.twiderex.model.ui.Option
+import com.twidere.twiderex.model.ui.StatusMetrics
+import com.twidere.twiderex.model.ui.UiCard
+import com.twidere.twiderex.model.ui.UiGeo
+import com.twidere.twiderex.model.ui.UiMedia
+import com.twidere.twiderex.model.ui.UiPoll
+import com.twidere.twiderex.model.ui.UiStatus
+import com.twidere.twiderex.model.ui.UiUrlEntity
+import com.twidere.twiderex.model.ui.UiUser
+import com.twidere.twiderex.model.ui.mastodon.MastodonMention
+import com.twidere.twiderex.model.ui.mastodon.MastodonStatusExtra
+import com.twidere.twiderex.model.ui.twitter.TwitterStatusExtra
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
+
+fun DbStatusV2.toUi(
+ user: UiUser,
+ media: List,
+ url: List,
+ reaction: DbStatusReaction?,
+ isGap: Boolean,
+ referenceStatus: Map = emptyMap(),
+): UiStatus {
+ var poll: UiPoll? = null
+ var sensitive = false
+ var spoilerText: String? = null
+ val extra = when (platformType) {
+ PlatformType.Twitter -> Json.decodeFromString(extra).toUi()
+ PlatformType.StatusNet -> TODO()
+ PlatformType.Fanfou -> TODO()
+ PlatformType.Mastodon -> Json.decodeFromString(extra).apply {
+ poll = this.poll?.toUi()
+ sensitive = this.sensitive
+ spoilerText = this.spoilerText
+ }.toUi()
+ }
+ return UiStatus(
+ statusId = statusId,
+ htmlText = htmlText,
+ timestamp = timestamp,
+ metrics = StatusMetrics(
+ retweet = retweetCount,
+ like = likeCount,
+ reply = replyCount,
+ ),
+ retweeted = reaction?.retweeted ?: false,
+ liked = reaction?.liked ?: false,
+ geo = UiGeo(
+ name = placeString ?: "",
+ lat = null,
+ long = null
+ ),
+ hasMedia = hasMedia,
+ user = user,
+ media = media,
+ isGap = isGap,
+ source = source,
+ url = url,
+ statusKey = statusKey,
+ rawText = rawText,
+ platformType = platformType,
+ extra = extra,
+ referenceStatus = referenceStatus,
+ card = previewCard?.toUi(),
+ poll = poll,
+ inReplyToStatusId = inReplyToStatusId,
+ inReplyToUserId = inReplyToStatusId,
+ sensitive = sensitive,
+ spoilerText = spoilerText
+ )
+}
+
+fun DbStatusWithMediaAndUser.toUi(
+ accountKey: MicroBlogKey,
+): UiStatus {
+ val reaction = reactions.firstOrNull { it.accountKey == accountKey }
+ return data.toUi(
+ user = user.toUi(),
+ media = media.toUi(),
+ url = url.toUi(),
+ isGap = false,
+ reaction = reaction
+ )
+}
+
+fun DbStatusWithReference.toUi(
+ accountKey: MicroBlogKey,
+) = with(status) {
+ val reaction = reactions.firstOrNull { it.accountKey == accountKey }
+ data.toUi(
+ user = user.toUi(),
+ media = media.toUi(),
+ isGap = false,
+ url = url.toUi(),
+ reaction = reaction,
+ referenceStatus = references.map {
+ it.reference.referenceType to it.status.toUi(
+ accountKey = accountKey
+ )
+ }.toMap()
+ )
+}
+
+fun DbPagingTimelineWithStatus.toUi(
+ accountKey: MicroBlogKey,
+) = with(status.status) {
+ val reaction = reactions.firstOrNull { it.accountKey == accountKey }
+ data.toUi(
+ user = user.toUi(),
+ media = media.toUi(),
+ isGap = timeline.isGap,
+ url = url.toUi(),
+ reaction = reaction,
+ referenceStatus = status.references.map {
+ it.reference.referenceType to it.status.toUi(
+ accountKey = accountKey
+ )
+ }.toMap()
+ )
+}
+
+fun DbTwitterStatusExtra.toUi() = TwitterStatusExtra(
+ reply_settings = reply_settings,
+ quoteCount = quoteCount
+)
+
+fun DbMastodonStatusExtra.toUi() = MastodonStatusExtra(
+ type = type,
+ emoji = emoji.toUi(),
+ visibility = visibility,
+ mentions = mentions?.toUi()
+)
+
+fun List.toUi() = map {
+ MastodonMention(
+ id = it.id,
+ username = it.username,
+ url = it.url,
+ acct = it.acct
+ )
+}
+
+fun DbPreviewCard.toUi() = UiCard(
+ link = link,
+ displayLink = displayLink,
+ title = title,
+ description = desc,
+ image = image
+)
+
+fun Poll.toUi() = id?.let {
+ UiPoll(
+ id = it,
+ options = options?.map { option ->
+ Option(
+ text = option.title ?: "",
+ count = option.votesCount ?: 0
+ )
+ } ?: emptyList(),
+ expired = expired ?: false,
+ expiresAt = expiresAt?.time,
+ multiple = multiple ?: false,
+ voted = voted ?: false,
+ votersCount = votersCount,
+ ownVotes = ownVotes,
+ votesCount = votesCount
+ )
+}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/transform/TrendTransform.kt b/app/src/main/kotlin/com/twidere/twiderex/model/transform/TrendTransform.kt
new file mode 100644
index 000000000..485c4ad42
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/transform/TrendTransform.kt
@@ -0,0 +1,44 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.model.transform
+
+import com.twidere.twiderex.db.model.DbTrendHistory
+import com.twidere.twiderex.db.model.DbTrendWithHistory
+import com.twidere.twiderex.model.ui.UiTrend
+import com.twidere.twiderex.model.ui.UiTrendHistory
+
+fun DbTrendWithHistory.toUi() = UiTrend(
+ trendKey = trend.trendKey,
+ displayName = trend.displayName,
+ url = trend.url,
+ query = trend.query,
+ volume = trend.volume,
+ history = history.map {
+ it.toUi()
+ }
+)
+
+fun DbTrendHistory.toUi() = UiTrendHistory(
+ trendKey = trendKey,
+ day = day,
+ uses = uses,
+ accounts = accounts
+)
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/transform/UrlEntityTransform.kt b/app/src/main/kotlin/com/twidere/twiderex/model/transform/UrlEntityTransform.kt
new file mode 100644
index 000000000..3850f8324
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/transform/UrlEntityTransform.kt
@@ -0,0 +1,29 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.model.transform
+
+import com.twidere.twiderex.db.model.DbUrlEntity
+import com.twidere.twiderex.model.ui.UiUrlEntity
+
+fun DbUrlEntity.toUi() = UiUrlEntity(
+ url, expandedUrl, displayUrl, title, description, image
+)
+fun List.toUi() = map { it.toUi() }
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/transform/UserTransform.kt b/app/src/main/kotlin/com/twidere/twiderex/model/transform/UserTransform.kt
new file mode 100644
index 000000000..760b6e5a8
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/transform/UserTransform.kt
@@ -0,0 +1,109 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.model.transform
+
+import com.twidere.twiderex.db.model.DbMastodonUserExtra
+import com.twidere.twiderex.db.model.DbTwitterUserExtra
+import com.twidere.twiderex.db.model.DbUser
+import com.twidere.twiderex.model.AmUser
+import com.twidere.twiderex.model.enums.PlatformType
+import com.twidere.twiderex.model.ui.UiUrlEntity
+import com.twidere.twiderex.model.ui.UiUser
+import com.twidere.twiderex.model.ui.UserMetrics
+import com.twidere.twiderex.model.ui.mastodon.Field
+import com.twidere.twiderex.model.ui.mastodon.MastodonUserExtra
+import com.twidere.twiderex.model.ui.twitter.TwitterUserExtra
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
+
+fun DbUser.toAmUser() =
+ AmUser(
+ userId = userId,
+ name = name,
+ userKey = userKey,
+ screenName = screenName,
+ profileImage = profileImage,
+ profileBackgroundImage = profileBackgroundImage,
+ followersCount = followersCount,
+ friendsCount = friendsCount,
+ listedCount = listedCount,
+ desc = rawDesc,
+ website = website,
+ location = location,
+ verified = verified,
+ isProtected = isProtected,
+ )
+
+fun DbUser.toUi() =
+ UiUser(
+ id = userId,
+ name = name,
+ screenName = screenName,
+ profileImage = profileImage,
+ profileBackgroundImage = profileBackgroundImage,
+ metrics = UserMetrics(
+ fans = followersCount,
+ follow = friendsCount,
+ listed = listedCount,
+ status = statusesCount,
+ ),
+ rawDesc = rawDesc,
+ htmlDesc = htmlDesc,
+ website = website,
+ location = location,
+ verified = verified,
+ protected = isProtected,
+ userKey = userKey,
+ platformType = platformType,
+ extra = when (platformType) {
+ PlatformType.Twitter -> Json.decodeFromString(extra).toUi()
+ PlatformType.StatusNet -> TODO()
+ PlatformType.Fanfou -> TODO()
+ PlatformType.Mastodon -> Json.decodeFromString(extra).toUi()
+ },
+ acct = acct,
+ )
+
+fun DbTwitterUserExtra.toUi() = TwitterUserExtra(
+ pinned_tweet_id = pinned_tweet_id,
+ url = url.map { url ->
+ UiUrlEntity(
+ url = url.displayUrl,
+ expandedUrl = url.expandedUrl,
+ displayUrl = url.displayUrl,
+ title = null,
+ description = null,
+ image = null
+ )
+ }
+)
+
+fun DbMastodonUserExtra.toUi() = MastodonUserExtra(
+ emoji = emoji.toUi(),
+ bot = bot,
+ locked = locked,
+ fields = fields.map { field ->
+ Field(
+ field.name,
+ field.value
+ )
+ }
+)
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/ComposeData.kt b/app/src/main/kotlin/com/twidere/twiderex/model/transform/WorkDataTransform.kt
similarity index 55%
rename from app/src/main/kotlin/com/twidere/twiderex/model/ComposeData.kt
rename to app/src/main/kotlin/com/twidere/twiderex/model/transform/WorkDataTransform.kt
index 4127fffde..1694ade18 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/model/ComposeData.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/transform/WorkDataTransform.kt
@@ -18,33 +18,28 @@
* You should have received a copy of the GNU General Public License
* along with Twidere X. If not, see .
*/
-package com.twidere.twiderex.model
+package com.twidere.twiderex.model.transform
import androidx.work.Data
import androidx.work.workDataOf
-import com.twidere.services.mastodon.model.Visibility
import com.twidere.twiderex.extensions.getNullableBoolean
import com.twidere.twiderex.extensions.getNullableDouble
+import com.twidere.twiderex.model.MicroBlogKey
+import com.twidere.twiderex.model.enums.MastodonVisibility
+import com.twidere.twiderex.model.job.ComposeData
+import com.twidere.twiderex.model.job.DirectMessageDeleteData
+import com.twidere.twiderex.model.job.DirectMessageSendData
+import com.twidere.twiderex.model.job.StatusResult
import com.twidere.twiderex.viewmodel.compose.ComposeType
import com.twidere.twiderex.viewmodel.compose.VoteExpired
-import java.util.UUID
-data class ComposeData(
- val content: String,
- val images: List,
- val composeType: ComposeType,
- val statusKey: MicroBlogKey? = null,
- val lat: Double? = null,
- val long: Double? = null,
- val draftId: String = UUID.randomUUID().toString(),
- val excludedReplyUserIds: List? = null,
- val voteOptions: List? = null,
- val voteExpired: VoteExpired? = null,
- val voteMultiple: Boolean? = null,
- val visibility: Visibility? = null,
- val isSensitive: Boolean? = null,
- val contentWarningText: String? = null,
- val isThreadMode: Boolean = false
+fun StatusResult.toWorkData() = workDataOf(
+ "statusKey" to statusKey.toString(),
+ "accountKey" to accountKey.toString(),
+ "liked" to liked,
+ "retweeted" to retweeted,
+ "retweetCount" to retweetCount,
+ "likeCount" to likeCount,
)
fun ComposeData.toWorkData() = workDataOf(
@@ -77,8 +72,40 @@ fun Data.toComposeData() = ComposeData(
voteOptions = getStringArray("voteOptions")?.toList(),
voteExpired = getString("voteExpired")?.let { VoteExpired.valueOf(it) },
voteMultiple = getNullableBoolean("voteMultiple"),
- visibility = getString("visibility")?.let { Visibility.valueOf(it) },
+ visibility = getString("visibility")?.let { MastodonVisibility.valueOf(it) },
isSensitive = getNullableBoolean("isSensitive"),
contentWarningText = getString("contentWarningText"),
isThreadMode = getBoolean("isThreadMode", false),
)
+
+fun DirectMessageDeleteData.toWorkData() = workDataOf(
+ "accountKey" to accountKey.toString(),
+ "conversationKey" to conversationKey.toString(),
+ "messageKey" to messageKey.toString(),
+ "messageId" to messageId,
+)
+
+fun Data.toDirectMessageDeleteData() = DirectMessageDeleteData(
+ messageId = getString("messageId") ?: "",
+ accountKey = MicroBlogKey.valueOf(getString("accountKey") ?: ""),
+ conversationKey = MicroBlogKey.valueOf(getString("conversationKey") ?: ""),
+ messageKey = MicroBlogKey.valueOf(getString("messageKey") ?: ""),
+)
+
+fun DirectMessageSendData.toWorkData() = workDataOf(
+ "text" to text,
+ "images" to images.toTypedArray(),
+ "recipientUserKey" to recipientUserKey.toString(),
+ "dratMessageKey" to draftMessageKey.toString(),
+ "conversationKey" to conversationKey.toString(),
+ "accountKey" to accountKey.toString(),
+)
+
+fun Data.toDirectMessageSendData() = DirectMessageSendData(
+ text = getString("text") ?: "",
+ images = getStringArray("images")?.toList() ?: emptyList(),
+ recipientUserKey = MicroBlogKey.valueOf(getString("recipientUserKey") ?: ""),
+ draftMessageKey = MicroBlogKey.valueOf(getString("dratMessageKey") ?: ""),
+ conversationKey = MicroBlogKey.valueOf(getString("conversationKey") ?: ""),
+ accountKey = MicroBlogKey.valueOf(getString("accountKey") ?: ""),
+)
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/ui/UiCard.kt b/app/src/main/kotlin/com/twidere/twiderex/model/ui/UiCard.kt
new file mode 100644
index 000000000..271444244
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/ui/UiCard.kt
@@ -0,0 +1,29 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.model.ui
+
+data class UiCard(
+ val link: String,
+ val displayLink: String?,
+ val title: String?,
+ val description: String?,
+ val image: String?,
+)
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/ui/UiDMConversation.kt b/app/src/main/kotlin/com/twidere/twiderex/model/ui/UiDMConversation.kt
index 0f258da98..2fe028df2 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/model/ui/UiDMConversation.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/ui/UiDMConversation.kt
@@ -21,10 +21,7 @@
package com.twidere.twiderex.model.ui
import com.twidere.twiderex.db.model.DbDMConversation
-import com.twidere.twiderex.db.model.DbDirectMessageConversationWithMessage
import com.twidere.twiderex.model.MicroBlogKey
-import com.twidere.twiderex.model.ui.UiDMConversation.Companion.toUi
-import com.twidere.twiderex.model.ui.UiDMEvent.Companion.toUi
data class UiDMConversation(
val accountKey: MicroBlogKey,
@@ -36,29 +33,9 @@ data class UiDMConversation(
val conversationSubName: String,
val conversationType: DbDMConversation.Type,
val recipientKey: MicroBlogKey,
-) {
- companion object {
- fun DbDMConversation.toUi() = UiDMConversation(
- accountKey = accountKey,
- conversationId = conversationId,
- conversationKey = conversationKey,
- conversationAvatar = conversationAvatar,
- conversationName = conversationName,
- conversationSubName = conversationSubName,
- conversationType = conversationType,
- recipientKey = recipientKey,
- )
- }
-}
+)
data class UiDMConversationWithLatestMessage(
val conversation: UiDMConversation,
val latestMessage: UiDMEvent
-) {
- companion object {
- fun DbDirectMessageConversationWithMessage.toUi() = UiDMConversationWithLatestMessage(
- conversation = conversation.toUi(),
- latestMessage = latestMessage.toUi()
- )
- }
-}
+)
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/ui/UiDMEvent.kt b/app/src/main/kotlin/com/twidere/twiderex/model/ui/UiDMEvent.kt
index 13a084532..4972e7372 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/model/ui/UiDMEvent.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/ui/UiDMEvent.kt
@@ -21,11 +21,7 @@
package com.twidere.twiderex.model.ui
import com.twidere.twiderex.db.model.DbDMEvent
-import com.twidere.twiderex.db.model.DbDMEventWithAttachments
import com.twidere.twiderex.model.MicroBlogKey
-import com.twidere.twiderex.model.ui.UiMedia.Companion.toUi
-import com.twidere.twiderex.model.ui.UiUrlEntity.Companion.toUi
-import com.twidere.twiderex.model.ui.UiUser.Companion.toUi
data class UiDMEvent(
val accountKey: MicroBlogKey,
@@ -48,24 +44,4 @@ data class UiDMEvent(
) {
val isInCome: Boolean
get() = recipientAccountKey == accountKey
-
- companion object {
- fun DbDMEventWithAttachments.toUi() = UiDMEvent(
- accountKey = message.accountKey,
- sortId = message.sortId,
- conversationKey = message.conversationKey,
- messageId = message.messageId,
- messageKey = message.messageKey,
- htmlText = message.htmlText,
- originText = message.originText,
- createdTimestamp = message.createdTimestamp,
- messageType = message.messageType,
- senderAccountKey = message.senderAccountKey,
- recipientAccountKey = message.recipientAccountKey,
- sendStatus = message.sendStatus,
- media = media.toUi(),
- urlEntity = urlEntity.toUi(),
- sender = sender.toUi()
- )
- }
}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/ui/UiEmoji.kt b/app/src/main/kotlin/com/twidere/twiderex/model/ui/UiEmojiCategory.kt
similarity index 75%
rename from app/src/main/kotlin/com/twidere/twiderex/model/ui/UiEmoji.kt
rename to app/src/main/kotlin/com/twidere/twiderex/model/ui/UiEmojiCategory.kt
index 7c157a0f6..d041c6290 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/model/ui/UiEmoji.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/ui/UiEmojiCategory.kt
@@ -20,15 +20,15 @@
*/
package com.twidere.twiderex.model.ui
-import com.twidere.services.mastodon.model.Emoji
+data class UiEmojiCategory(
+ val category: String?,
+ val emoji: List
+)
data class UiEmoji(
- val category: String?,
- val emoji: List
-) {
- companion object {
- fun List.toUi(): List = groupBy({ it.category }, { it }).map {
- UiEmoji(if (it.key.isNullOrEmpty()) null else it.key, it.value)
- }
- }
-}
+ val shortcode: String? = null,
+ val url: String? = null,
+ val staticURL: String? = null,
+ val visibleInPicker: Boolean? = null,
+ val category: String? = null
+)
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/ui/UiGeo.kt b/app/src/main/kotlin/com/twidere/twiderex/model/ui/UiGeo.kt
new file mode 100644
index 000000000..d336f2a8e
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/ui/UiGeo.kt
@@ -0,0 +1,27 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.model.ui
+
+data class UiGeo(
+ val name: String,
+ val lat: Long? = null,
+ val long: Long? = null,
+)
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/ui/UiList.kt b/app/src/main/kotlin/com/twidere/twiderex/model/ui/UiList.kt
index 5aa46526f..9d523c4a6 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/model/ui/UiList.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/ui/UiList.kt
@@ -20,7 +20,6 @@
*/
package com.twidere.twiderex.model.ui
import androidx.compose.runtime.Immutable
-import com.twidere.twiderex.db.model.DbList
import com.twidere.twiderex.model.MicroBlogKey
@Immutable
@@ -58,20 +57,6 @@ data class UiList(
isFollowed = isFollowed,
allowToSubscribe = true,
)
-
- fun DbList.toUi() =
- UiList(
- id = listId,
- ownerId = ownerId,
- listKey = listKey,
- accountKey = accountKey,
- title = title,
- descriptions = description,
- mode = mode,
- replyPolicy = replyPolicy,
- isFollowed = isFollowed,
- allowToSubscribe = allowToSubscribe,
- )
}
}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/ui/UiMedia.kt b/app/src/main/kotlin/com/twidere/twiderex/model/ui/UiMedia.kt
index 768bd07db..0c959c218 100644
--- a/app/src/main/kotlin/com/twidere/twiderex/model/ui/UiMedia.kt
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/ui/UiMedia.kt
@@ -25,8 +25,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.res.painterResource
import com.twidere.twiderex.R
-import com.twidere.twiderex.db.model.DbMedia
-import com.twidere.twiderex.model.MediaType
+import com.twidere.twiderex.model.enums.MediaType
@Immutable
data class UiMedia(
@@ -73,18 +72,5 @@ data class UiMedia(
altText = "",
),
)
-
- fun List.toUi() = sortedBy { it.order }.map {
- UiMedia(
- url = it.url,
- mediaUrl = it.mediaUrl,
- previewUrl = it.previewUrl,
- type = it.type,
- width = it.width,
- height = it.height,
- pageUrl = it.pageUrl,
- altText = it.altText,
- )
- }
}
}
diff --git a/app/src/main/kotlin/com/twidere/twiderex/model/ui/UiPoll.kt b/app/src/main/kotlin/com/twidere/twiderex/model/ui/UiPoll.kt
new file mode 100644
index 000000000..883245053
--- /dev/null
+++ b/app/src/main/kotlin/com/twidere/twiderex/model/ui/UiPoll.kt
@@ -0,0 +1,38 @@
+/*
+ * Twidere X
+ *
+ * Copyright (C) 2020-2021 Tlaster
+ *
+ * This file is part of Twidere X.
+ *
+ * Twidere X is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Twidere X is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Twidere X. If not, see .
+ */
+package com.twidere.twiderex.model.ui
+
+data class UiPoll(
+ val id: String,
+ val options: List