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