Compare commits
3 Commits
master
...
direct_mes
Author | SHA1 | Date | |
---|---|---|---|
|
7f3ab1e4a6 | ||
|
0fe2c54940 | ||
|
86b0fe1c60 |
.gitlab-ci.yml.gitmodulesREADME.md
app
build.gradle
schemas/org.pixeldroid.app.utils.db.AppDatabase
src/main
AndroidManifest.xml
java/org/pixeldroid/app
LoginActivity.ktMainActivity.kt
directMessages
directmessages
BounceEdgeEffectFactory.ktConversationActivity.ktConversationFragment.ktConversationRemoteMediator.ktDirectMessagesActivity.ktDirectMessagesFragment.kt
login
main
postCreation
PostCreationActivity.ktPostCreationFragment.ktPostCreationViewModel.ktPostSubmissionFragment.kt
camera
posts
AlbumActivity.ktAlbumViewModel.ktNestedScrollableHost.ktPostActivity.ktReportActivity.ktReportActivityViewModel.ktStatusViewHolder.kt
feeds
CommonFeedFragmentUtils.kt
cachedFeeds
uncachedFeeds
profile
CollectionActivity.ktEditProfileActivity.ktEditProfileViewModel.ktFollowsActivity.ktProfileActivity.ktProfileFeedFragment.kt
settings
ArrangeTabsFragment.ktArrangeTabsViewModel.ktLanguageSettingFragment.ktSettingsActivity.ktTutorialSettingsDialog.kt
stories
utils
BaseActivity.ktBaseFragment.ktPixelDroidApplication.ktUtils.kt
api
db
di
notificationsWorker
res/drawable
@ -126,7 +126,7 @@ fdroid build:
|
||||
- .gradle
|
||||
script:
|
||||
# Put the correct versionName and versionCode in the .fdroid.yml
|
||||
- sed -e "s/\${versionName}/$(./gradlew -q printVersionName)/" -e "s/\${versionCode}/$(./gradlew -q printVersionCode)/" .fdroid.yml.template > .fdroid.yml
|
||||
- sed -e "s/\${versionName}/$(grep "versionName " app/build.gradle | awk '{print $2}' | tr -d \")$(grep "versionCode" app/build.gradle -m 1 | awk '{print $2}')/" -e "s/\${versionCode}/$(grep "versionCode" app/build.gradle -m 1 | awk '{print $2}')/" .fdroid.yml.template > .fdroid.yml
|
||||
- rm .fdroid.yml.template
|
||||
# each `fdroid build --on-server` run expects sudo, then uninstalls it
|
||||
- set -x
|
||||
|
2
.gitmodules
vendored
2
.gitmodules
vendored
@ -3,4 +3,4 @@
|
||||
url = https://gitlab.com/artectrex/scrambler.git
|
||||
[submodule "pixel_common"]
|
||||
path = pixel_common
|
||||
url = https://gitlab.shinice.net/pixeldroid/pixel_common.git
|
||||
url = git@gitlab.shinice.net:pixeldroid/pixel_common.git
|
||||
|
@ -9,16 +9,13 @@ Free (as in freedom) Android client for Pixelfed, the federated image sharing pl
|
||||
<img src="https://pixeldroid.org/badge-fdroid.png" alt="Get it on F-Droid" width="206">
|
||||
</a>
|
||||
|
||||
Come talk to us on Matrix, at <a href="https://matrix.to/#/#pixeldroid:gnugen.ch">#pixeldroid:gnugen.ch</a> !
|
||||
|
||||
## 🔧 Compiling the code yourself
|
||||
If you want to try out PixelDroid on your own device, you can compile the source code yourself. To do that you can install [Android Studio](https://developer.android.com/studio/).
|
||||
|
||||
## 🎨 Art attribution
|
||||
Our mascot was commissioned using funds from NLnet. The original file is `pixeldroid_mascot.svg` and it is adapted to work as an Android Drawable. This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/) (CC BY-SA).
|
||||
|
||||
Various works have been used from the pixelfed branding repository ( https://github.com/pixelfed/brand-assets ).
|
||||
|
||||
Various works have been used from the pixelfed branding repository ( https://github.com/pixelfed/brand-assets ). In addition, a drawing of a red panda is used for some error messages ( https://thenounproject.com/search/?q=red+panda&i=2877785 ).
|
||||
## 🤝 Contribute
|
||||
If you want to contribute, you can check out [CONTRIBUTING.md](CONTRIBUTING.md) and/or [TRANSLATION.md](TRANSLATION.md)
|
||||
|
||||
|
190
app/build.gradle
190
app/build.gradle
@ -1,33 +1,15 @@
|
||||
import com.android.build.api.dsl.ManagedVirtualDevice
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("com.google.dagger.hilt.android")
|
||||
id("kotlin-android")
|
||||
id("jacoco")
|
||||
id("kotlin-parcelize")
|
||||
id("com.google.devtools.ksp")
|
||||
}
|
||||
// Map for the version code that gives each ABI a value.
|
||||
ext.abiCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, x86: 3, x86_64: 4]
|
||||
|
||||
//Different version codes per architecture (for F-Droid support)
|
||||
android.applicationVariants.configureEach { variant ->
|
||||
variant.outputs.each { output ->
|
||||
def baseAbiVersionCode = project.ext.abiCodes.get(output.getFilter(com.android.build.OutputFile.ABI))
|
||||
if (baseAbiVersionCode != null) {
|
||||
output.versionCodeOverride = (100 * project.android.defaultConfig.versionCode) + baseAbiVersionCode
|
||||
} else {
|
||||
output.versionCodeOverride = 100 * project.android.defaultConfig.versionCode
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'jacoco'
|
||||
apply plugin: "kotlin-parcelize"
|
||||
apply plugin: 'com.google.devtools.ksp'
|
||||
|
||||
android {
|
||||
|
||||
namespace 'org.pixeldroid.app'
|
||||
compileSdk 35
|
||||
compileSdk 34
|
||||
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
@ -45,18 +27,14 @@ android {
|
||||
}
|
||||
defaultConfig {
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 35
|
||||
versionCode 39
|
||||
versionCode 26
|
||||
targetSdkVersion 34
|
||||
versionName "1.0.beta" + versionCode
|
||||
|
||||
//TODO add resConfigs("en", "fr", "ja",...) ?
|
||||
|
||||
testInstrumentationRunner "org.pixeldroid.app.testUtility.TestRunner"
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
}
|
||||
}
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/java'
|
||||
@ -99,30 +77,6 @@ android {
|
||||
proguardFiles 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
splits {
|
||||
// Configures multiple APKs based on ABI.
|
||||
abi {
|
||||
|
||||
// Enables building multiple APKs per ABI.
|
||||
enable true
|
||||
|
||||
// By default all ABIs are included, so use reset() and include to specify that we only
|
||||
// want APKs for "x86", "x86_64", "arm64-v8a" and "armeabi-v7a".
|
||||
|
||||
// Resets the list of ABIs for Gradle to create APKs for to none.
|
||||
reset()
|
||||
|
||||
// Specifies a list of ABIs for Gradle to create APKs for.
|
||||
//noinspection ChromeOsAbiSupport
|
||||
include project.ext.abiCodes.keySet() as String[]
|
||||
|
||||
// Specifies that we don't want to also generate a universal APK that includes all ABIs.
|
||||
universalApk false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a string with the application_id (available in xml etc)
|
||||
*/
|
||||
@ -166,56 +120,43 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('printVersionName') {
|
||||
doLast {
|
||||
println android.defaultConfig.versionName
|
||||
}
|
||||
}
|
||||
tasks.register('printVersionCode') {
|
||||
doLast {
|
||||
println android.defaultConfig.versionCode
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation 'androidx.hilt:hilt-common:1.2.0'
|
||||
implementation 'androidx.hilt:hilt-work:1.2.0'
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.3'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
||||
|
||||
/**
|
||||
* AndroidX dependencies:
|
||||
*/
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
||||
implementation 'androidx.core:core-ktx:1.15.0'
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.8.4'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.8.4'
|
||||
implementation "androidx.browser:browser:1.8.0"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'
|
||||
implementation "androidx.browser:browser:1.7.0"
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.8.4'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.8.4'
|
||||
implementation 'androidx.paging:paging-runtime-ktx:3.3.4'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.7'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.7'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.7'
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:2.8.7"
|
||||
implementation "androidx.annotation:annotation:1.9.1"
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'
|
||||
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.2'
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:2.6.2"
|
||||
implementation "androidx.annotation:annotation:1.7.1"
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||
implementation "androidx.activity:activity-ktx:1.9.3"
|
||||
implementation 'androidx.fragment:fragment-ktx:1.8.5'
|
||||
implementation 'androidx.work:work-runtime-ktx:2.10.0'
|
||||
implementation 'androidx.media2:media2-widget:1.3.0'
|
||||
implementation 'androidx.media2:media2-player:1.3.0'
|
||||
implementation "androidx.activity:activity-ktx:1.8.2"
|
||||
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
||||
implementation 'androidx.work:work-runtime-ktx:2.9.0'
|
||||
implementation 'androidx.media2:media2-widget:1.2.1'
|
||||
implementation 'androidx.media2:media2-player:1.2.1'
|
||||
|
||||
|
||||
// Use the most recent version of CameraX
|
||||
def cameraX_version = '1.4.0'
|
||||
def cameraX_version = '1.3.1'
|
||||
implementation "androidx.camera:camera-core:$cameraX_version"
|
||||
implementation "androidx.camera:camera-camera2:$cameraX_version"
|
||||
// CameraX Lifecycle library
|
||||
@ -237,27 +178,21 @@ dependencies {
|
||||
|
||||
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
|
||||
|
||||
//Interactive tutorial
|
||||
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.14.0'
|
||||
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
implementation 'com.google.android.material:material:1.11.0'
|
||||
|
||||
//Dagger (dependency injection)
|
||||
implementation 'com.google.dagger:dagger:2.52'
|
||||
ksp 'com.google.dagger:dagger-compiler:2.52'
|
||||
implementation 'com.google.dagger:dagger:2.48'
|
||||
ksp 'com.google.dagger:dagger-compiler:2.48'
|
||||
|
||||
implementation('com.google.dagger:hilt-android:2.52')
|
||||
ksp 'com.google.dagger:hilt-compiler:2.52'
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.11.0'
|
||||
implementation 'com.squareup.retrofit2:converter-gson:2.11.0'
|
||||
implementation 'com.squareup.retrofit2:adapter-rxjava3:2.11.0'
|
||||
implementation 'io.reactivex.rxjava3:rxjava:3.1.9'
|
||||
implementation 'io.reactivex.rxjava3:rxandroid:3.0.2'
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
|
||||
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
|
||||
implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0'
|
||||
implementation 'io.reactivex.rxjava3:rxjava:3.1.6'
|
||||
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
|
||||
implementation 'com.github.connyduck:sparkbutton:4.1.0'
|
||||
|
||||
implementation 'org.pixeldroid.pixeldroid:android-media-editor:4.3'
|
||||
implementation 'org.pixeldroid.pixeldroid:android-media-editor:1.5'
|
||||
implementation project(path: ':scrambler')
|
||||
implementation project(path: ':pixel_common')
|
||||
|
||||
@ -265,20 +200,26 @@ dependencies {
|
||||
exclude group: "com.android.support"
|
||||
}
|
||||
|
||||
implementation 'com.github.bumptech.glide:okhttp3-integration:4.16.0'
|
||||
implementation('com.github.bumptech.glide:recyclerview-integration:4.16.0') {
|
||||
implementation 'com.github.bumptech.glide:okhttp3-integration:4.14.2'
|
||||
implementation('com.github.bumptech.glide:recyclerview-integration:4.14.2') {
|
||||
// Excludes the support library because it's already included by Glide.
|
||||
transitive = false
|
||||
}
|
||||
implementation 'com.github.bumptech.glide:annotations:4.16.0'
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
|
||||
ksp 'com.github.bumptech.glide:ksp:4.16.0'
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.14.2'
|
||||
ksp 'com.github.bumptech.glide:ksp:4.14.2'
|
||||
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
|
||||
implementation 'com.mikepenz:materialdrawer:9.0.2'
|
||||
implementation 'com.mikepenz:materialdrawer:9.0.1'
|
||||
// Add for NavController support
|
||||
implementation 'com.mikepenz:materialdrawer-nav:9.0.2'
|
||||
implementation 'com.mikepenz:materialdrawer-nav:9.0.1'
|
||||
|
||||
//iconics
|
||||
implementation 'com.mikepenz:iconics-core:5.4.0'
|
||||
implementation 'com.mikepenz:materialdrawer-iconics:9.0.1'
|
||||
implementation 'com.mikepenz:iconics-views:5.4.0'
|
||||
implementation 'com.mikepenz:google-material-typeface:4.0.0.2-kotlin@aar'
|
||||
|
||||
implementation 'com.github.ligi:tracedroid:4.1'
|
||||
|
||||
@ -294,36 +235,33 @@ dependencies {
|
||||
androidTestImplementation 'com.linkedin.testbutler:test-butler-library:2.2.1'
|
||||
androidTestUtil 'com.linkedin.testbutler:test-butler-app:2.2.1'
|
||||
|
||||
androidTestImplementation 'androidx.work:work-testing:2.10.0'
|
||||
testImplementation 'org.wiremock:wiremock:3.9.1'
|
||||
androidTestImplementation 'androidx.work:work-testing:2.9.0'
|
||||
testImplementation 'com.github.tomakehurst:wiremock-jre8:2.34.0'
|
||||
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation "androidx.room:room-testing:$room_version"
|
||||
|
||||
|
||||
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.3.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.6.2'
|
||||
androidTestImplementation 'androidx.test:rules:1.6.1'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
|
||||
androidTestImplementation 'androidx.test:runner:1.6.2'
|
||||
androidTestImplementation 'androidx.test:rules:1.6.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.6.1'
|
||||
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.5.2'
|
||||
androidTestImplementation 'androidx.test:rules:1.5.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test:runner:1.5.2'
|
||||
androidTestImplementation 'androidx.test:rules:1.5.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.1'
|
||||
androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.0.2'
|
||||
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0'
|
||||
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.9.2'
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
tasks.withType(Test).configureEach {
|
||||
jacoco.includeNoLocationClasses = true
|
||||
jacoco.excludes = ['jdk.internal.*']
|
||||
}
|
||||
|
||||
|
||||
tasks.register('jacocoTestReport', JacocoReport) {
|
||||
dependsOn['connectedStagingAndroidTest', 'testStagingUnitTest', 'createStagingCoverageReport']
|
||||
task jacocoTestReport(type: JacocoReport, dependsOn: ['connectedStagingAndroidTest', 'testStagingUnitTest', 'createStagingCoverageReport']) {
|
||||
|
||||
reports {
|
||||
xml.required = true
|
||||
|
@ -1,991 +0,0 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 10,
|
||||
"identityHash": "5ee0e8fbaef28650cbea6670e24e08bb",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "instances",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uri` TEXT NOT NULL, `title` TEXT NOT NULL, `maxStatusChars` INTEGER NOT NULL, `maxPhotoSize` INTEGER NOT NULL, `maxVideoSize` INTEGER NOT NULL, `albumLimit` INTEGER NOT NULL, `videoEnabled` INTEGER NOT NULL, `pixelfed` INTEGER NOT NULL, PRIMARY KEY(`uri`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "uri",
|
||||
"columnName": "uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "maxStatusChars",
|
||||
"columnName": "maxStatusChars",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "maxPhotoSize",
|
||||
"columnName": "maxPhotoSize",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "maxVideoSize",
|
||||
"columnName": "maxVideoSize",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "albumLimit",
|
||||
"columnName": "albumLimit",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "videoEnabled",
|
||||
"columnName": "videoEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pixelfed",
|
||||
"columnName": "pixelfed",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"uri"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "users",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `username` TEXT NOT NULL, `display_name` TEXT NOT NULL, `avatar_static` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accessToken` TEXT NOT NULL, `refreshToken` TEXT, `clientId` TEXT NOT NULL, `clientSecret` TEXT NOT NULL, PRIMARY KEY(`user_id`, `instance_uri`), FOREIGN KEY(`instance_uri`) REFERENCES `instances`(`uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "user_id",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance_uri",
|
||||
"columnName": "instance_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "username",
|
||||
"columnName": "username",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "display_name",
|
||||
"columnName": "display_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "avatar_static",
|
||||
"columnName": "avatar_static",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isActive",
|
||||
"columnName": "isActive",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accessToken",
|
||||
"columnName": "accessToken",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "refreshToken",
|
||||
"columnName": "refreshToken",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "clientId",
|
||||
"columnName": "clientId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "clientSecret",
|
||||
"columnName": "clientSecret",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_users_instance_uri",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"instance_uri"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_users_instance_uri` ON `${TABLE_NAME}` (`instance_uri`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "instances",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"instance_uri"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"uri"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "homePosts",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `id` TEXT NOT NULL, `uri` TEXT, `created_at` TEXT, `account` TEXT, `content` TEXT, `visibility` TEXT, `sensitive` INTEGER, `spoiler_text` TEXT, `media_attachments` TEXT, `application` TEXT, `mentions` TEXT, `tags` TEXT, `emojis` TEXT, `reblogs_count` INTEGER, `favourites_count` INTEGER, `replies_count` INTEGER, `url` TEXT, `in_reply_to_id` TEXT, `in_reply_to_account` TEXT, `reblog` TEXT, `poll` TEXT, `card` TEXT, `language` TEXT, `text` TEXT, `favourited` INTEGER, `reblogged` INTEGER, `muted` INTEGER, `bookmarked` INTEGER, `pinned` INTEGER, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "user_id",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance_uri",
|
||||
"columnName": "instance_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "uri",
|
||||
"columnName": "uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "created_at",
|
||||
"columnName": "created_at",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "account",
|
||||
"columnName": "account",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "content",
|
||||
"columnName": "content",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "visibility",
|
||||
"columnName": "visibility",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "sensitive",
|
||||
"columnName": "sensitive",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "spoiler_text",
|
||||
"columnName": "spoiler_text",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "media_attachments",
|
||||
"columnName": "media_attachments",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "application",
|
||||
"columnName": "application",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "mentions",
|
||||
"columnName": "mentions",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "tags",
|
||||
"columnName": "tags",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "emojis",
|
||||
"columnName": "emojis",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblogs_count",
|
||||
"columnName": "reblogs_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "favourites_count",
|
||||
"columnName": "favourites_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "replies_count",
|
||||
"columnName": "replies_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "in_reply_to_id",
|
||||
"columnName": "in_reply_to_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "in_reply_to_account",
|
||||
"columnName": "in_reply_to_account",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblog",
|
||||
"columnName": "reblog",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "poll",
|
||||
"columnName": "poll",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "card",
|
||||
"columnName": "card",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "language",
|
||||
"columnName": "language",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "text",
|
||||
"columnName": "text",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "favourited",
|
||||
"columnName": "favourited",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblogged",
|
||||
"columnName": "reblogged",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "muted",
|
||||
"columnName": "muted",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "bookmarked",
|
||||
"columnName": "bookmarked",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "pinned",
|
||||
"columnName": "pinned",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_homePosts_user_id_instance_uri",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_homePosts_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "publicPosts",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `id` TEXT NOT NULL, `uri` TEXT, `created_at` TEXT, `account` TEXT, `content` TEXT, `visibility` TEXT, `sensitive` INTEGER, `spoiler_text` TEXT, `media_attachments` TEXT, `application` TEXT, `mentions` TEXT, `tags` TEXT, `emojis` TEXT, `reblogs_count` INTEGER, `favourites_count` INTEGER, `replies_count` INTEGER, `url` TEXT, `in_reply_to_id` TEXT, `in_reply_to_account` TEXT, `reblog` TEXT, `poll` TEXT, `card` TEXT, `language` TEXT, `text` TEXT, `favourited` INTEGER, `reblogged` INTEGER, `muted` INTEGER, `bookmarked` INTEGER, `pinned` INTEGER, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "user_id",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance_uri",
|
||||
"columnName": "instance_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "uri",
|
||||
"columnName": "uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "created_at",
|
||||
"columnName": "created_at",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "account",
|
||||
"columnName": "account",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "content",
|
||||
"columnName": "content",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "visibility",
|
||||
"columnName": "visibility",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "sensitive",
|
||||
"columnName": "sensitive",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "spoiler_text",
|
||||
"columnName": "spoiler_text",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "media_attachments",
|
||||
"columnName": "media_attachments",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "application",
|
||||
"columnName": "application",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "mentions",
|
||||
"columnName": "mentions",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "tags",
|
||||
"columnName": "tags",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "emojis",
|
||||
"columnName": "emojis",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblogs_count",
|
||||
"columnName": "reblogs_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "favourites_count",
|
||||
"columnName": "favourites_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "replies_count",
|
||||
"columnName": "replies_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "in_reply_to_id",
|
||||
"columnName": "in_reply_to_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "in_reply_to_account",
|
||||
"columnName": "in_reply_to_account",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblog",
|
||||
"columnName": "reblog",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "poll",
|
||||
"columnName": "poll",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "card",
|
||||
"columnName": "card",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "language",
|
||||
"columnName": "language",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "text",
|
||||
"columnName": "text",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "favourited",
|
||||
"columnName": "favourited",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblogged",
|
||||
"columnName": "reblogged",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "muted",
|
||||
"columnName": "muted",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "bookmarked",
|
||||
"columnName": "bookmarked",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "pinned",
|
||||
"columnName": "pinned",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_publicPosts_user_id_instance_uri",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_publicPosts_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "notifications",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` TEXT, `created_at` TEXT, `account` TEXT, `status` TEXT, `user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "created_at",
|
||||
"columnName": "created_at",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "account",
|
||||
"columnName": "account",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "status",
|
||||
"columnName": "status",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "user_id",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance_uri",
|
||||
"columnName": "instance_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_notifications_user_id_instance_uri",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_notifications_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "tabsChecked",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`index` INTEGER NOT NULL, `user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `tab` TEXT NOT NULL, `checked` INTEGER NOT NULL, `filter` TEXT, PRIMARY KEY(`index`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "index",
|
||||
"columnName": "index",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "user_id",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance_uri",
|
||||
"columnName": "instance_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tab",
|
||||
"columnName": "tab",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "checked",
|
||||
"columnName": "checked",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "filter",
|
||||
"columnName": "filter",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"index",
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_tabsChecked_user_id_instance_uri",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_tabsChecked_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "directMessages",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `unread` INTEGER, `accounts` TEXT, `last_status` TEXT, `user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "unread",
|
||||
"columnName": "unread",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "accounts",
|
||||
"columnName": "accounts",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "last_status",
|
||||
"columnName": "last_status",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "user_id",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance_uri",
|
||||
"columnName": "instance_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_directMessages_user_id_instance_uri",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_directMessages_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "directMessagesThreads",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `hidden` INTEGER, `isAuthor` INTEGER, `type` TEXT, `text` TEXT, `media` TEXT, `carousel` TEXT, `created_at` TEXT, `timeAgo` TEXT, `reportId` TEXT, `conversationsId` TEXT NOT NULL, `user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, PRIMARY KEY(`id`, `conversationsId`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "hidden",
|
||||
"columnName": "hidden",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAuthor",
|
||||
"columnName": "isAuthor",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "text",
|
||||
"columnName": "text",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "media",
|
||||
"columnName": "media",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "carousel",
|
||||
"columnName": "carousel",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "created_at",
|
||||
"columnName": "created_at",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "timeAgo",
|
||||
"columnName": "timeAgo",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reportId",
|
||||
"columnName": "reportId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "conversationsId",
|
||||
"columnName": "conversationsId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "user_id",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance_uri",
|
||||
"columnName": "instance_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"conversationsId",
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_directMessagesThreads_user_id_instance_uri_conversationsId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id",
|
||||
"instance_uri",
|
||||
"conversationsId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_directMessagesThreads_user_id_instance_uri_conversationsId` ON `${TABLE_NAME}` (`user_id`, `instance_uri`, `conversationsId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"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, '5ee0e8fbaef28650cbea6670e24e08bb')"
|
||||
]
|
||||
}
|
||||
}
|
@ -1,710 +0,0 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 6,
|
||||
"identityHash": "365491e03fadf81e596b11716640518c",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "instances",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uri` TEXT NOT NULL, `title` TEXT NOT NULL, `maxStatusChars` INTEGER NOT NULL, `maxPhotoSize` INTEGER NOT NULL, `maxVideoSize` INTEGER NOT NULL, `albumLimit` INTEGER NOT NULL, `videoEnabled` INTEGER NOT NULL, `pixelfed` INTEGER NOT NULL, PRIMARY KEY(`uri`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "uri",
|
||||
"columnName": "uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "maxStatusChars",
|
||||
"columnName": "maxStatusChars",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "maxPhotoSize",
|
||||
"columnName": "maxPhotoSize",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "maxVideoSize",
|
||||
"columnName": "maxVideoSize",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "albumLimit",
|
||||
"columnName": "albumLimit",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "videoEnabled",
|
||||
"columnName": "videoEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pixelfed",
|
||||
"columnName": "pixelfed",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"uri"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "users",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `username` TEXT NOT NULL, `display_name` TEXT NOT NULL, `avatar_static` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accessToken` TEXT NOT NULL, `refreshToken` TEXT, `clientId` TEXT NOT NULL, `clientSecret` TEXT NOT NULL, PRIMARY KEY(`user_id`, `instance_uri`), FOREIGN KEY(`instance_uri`) REFERENCES `instances`(`uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "user_id",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance_uri",
|
||||
"columnName": "instance_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "username",
|
||||
"columnName": "username",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "display_name",
|
||||
"columnName": "display_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "avatar_static",
|
||||
"columnName": "avatar_static",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isActive",
|
||||
"columnName": "isActive",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accessToken",
|
||||
"columnName": "accessToken",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "refreshToken",
|
||||
"columnName": "refreshToken",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "clientId",
|
||||
"columnName": "clientId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "clientSecret",
|
||||
"columnName": "clientSecret",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_users_instance_uri",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"instance_uri"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_users_instance_uri` ON `${TABLE_NAME}` (`instance_uri`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "instances",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"instance_uri"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"uri"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "homePosts",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `id` TEXT NOT NULL, `uri` TEXT, `created_at` TEXT, `account` TEXT, `content` TEXT, `visibility` TEXT, `sensitive` INTEGER, `spoiler_text` TEXT, `media_attachments` TEXT, `application` TEXT, `mentions` TEXT, `tags` TEXT, `emojis` TEXT, `reblogs_count` INTEGER, `favourites_count` INTEGER, `replies_count` INTEGER, `url` TEXT, `in_reply_to_id` TEXT, `in_reply_to_account` TEXT, `reblog` TEXT, `poll` TEXT, `card` TEXT, `language` TEXT, `text` TEXT, `favourited` INTEGER, `reblogged` INTEGER, `muted` INTEGER, `bookmarked` INTEGER, `pinned` INTEGER, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "user_id",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance_uri",
|
||||
"columnName": "instance_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "uri",
|
||||
"columnName": "uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "created_at",
|
||||
"columnName": "created_at",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "account",
|
||||
"columnName": "account",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "content",
|
||||
"columnName": "content",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "visibility",
|
||||
"columnName": "visibility",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "sensitive",
|
||||
"columnName": "sensitive",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "spoiler_text",
|
||||
"columnName": "spoiler_text",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "media_attachments",
|
||||
"columnName": "media_attachments",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "application",
|
||||
"columnName": "application",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "mentions",
|
||||
"columnName": "mentions",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "tags",
|
||||
"columnName": "tags",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "emojis",
|
||||
"columnName": "emojis",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblogs_count",
|
||||
"columnName": "reblogs_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "favourites_count",
|
||||
"columnName": "favourites_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "replies_count",
|
||||
"columnName": "replies_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "in_reply_to_id",
|
||||
"columnName": "in_reply_to_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "in_reply_to_account",
|
||||
"columnName": "in_reply_to_account",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblog",
|
||||
"columnName": "reblog",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "poll",
|
||||
"columnName": "poll",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "card",
|
||||
"columnName": "card",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "language",
|
||||
"columnName": "language",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "text",
|
||||
"columnName": "text",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "favourited",
|
||||
"columnName": "favourited",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblogged",
|
||||
"columnName": "reblogged",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "muted",
|
||||
"columnName": "muted",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "bookmarked",
|
||||
"columnName": "bookmarked",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "pinned",
|
||||
"columnName": "pinned",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_homePosts_user_id_instance_uri",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_homePosts_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "publicPosts",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `id` TEXT NOT NULL, `uri` TEXT, `created_at` TEXT, `account` TEXT, `content` TEXT, `visibility` TEXT, `sensitive` INTEGER, `spoiler_text` TEXT, `media_attachments` TEXT, `application` TEXT, `mentions` TEXT, `tags` TEXT, `emojis` TEXT, `reblogs_count` INTEGER, `favourites_count` INTEGER, `replies_count` INTEGER, `url` TEXT, `in_reply_to_id` TEXT, `in_reply_to_account` TEXT, `reblog` TEXT, `poll` TEXT, `card` TEXT, `language` TEXT, `text` TEXT, `favourited` INTEGER, `reblogged` INTEGER, `muted` INTEGER, `bookmarked` INTEGER, `pinned` INTEGER, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "user_id",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance_uri",
|
||||
"columnName": "instance_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "uri",
|
||||
"columnName": "uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "created_at",
|
||||
"columnName": "created_at",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "account",
|
||||
"columnName": "account",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "content",
|
||||
"columnName": "content",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "visibility",
|
||||
"columnName": "visibility",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "sensitive",
|
||||
"columnName": "sensitive",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "spoiler_text",
|
||||
"columnName": "spoiler_text",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "media_attachments",
|
||||
"columnName": "media_attachments",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "application",
|
||||
"columnName": "application",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "mentions",
|
||||
"columnName": "mentions",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "tags",
|
||||
"columnName": "tags",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "emojis",
|
||||
"columnName": "emojis",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblogs_count",
|
||||
"columnName": "reblogs_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "favourites_count",
|
||||
"columnName": "favourites_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "replies_count",
|
||||
"columnName": "replies_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "in_reply_to_id",
|
||||
"columnName": "in_reply_to_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "in_reply_to_account",
|
||||
"columnName": "in_reply_to_account",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblog",
|
||||
"columnName": "reblog",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "poll",
|
||||
"columnName": "poll",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "card",
|
||||
"columnName": "card",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "language",
|
||||
"columnName": "language",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "text",
|
||||
"columnName": "text",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "favourited",
|
||||
"columnName": "favourited",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblogged",
|
||||
"columnName": "reblogged",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "muted",
|
||||
"columnName": "muted",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "bookmarked",
|
||||
"columnName": "bookmarked",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "pinned",
|
||||
"columnName": "pinned",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_publicPosts_user_id_instance_uri",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_publicPosts_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "notifications",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` TEXT, `created_at` TEXT, `account` TEXT, `status` TEXT, `user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "created_at",
|
||||
"columnName": "created_at",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "account",
|
||||
"columnName": "account",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "status",
|
||||
"columnName": "status",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "user_id",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance_uri",
|
||||
"columnName": "instance_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_notifications_user_id_instance_uri",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_notifications_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"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, '365491e03fadf81e596b11716640518c')"
|
||||
]
|
||||
}
|
||||
}
|
@ -1,781 +0,0 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 7,
|
||||
"identityHash": "cd0310b10eff0e3e961b3cd7a8172b81",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "instances",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uri` TEXT NOT NULL, `title` TEXT NOT NULL, `maxStatusChars` INTEGER NOT NULL, `maxPhotoSize` INTEGER NOT NULL, `maxVideoSize` INTEGER NOT NULL, `albumLimit` INTEGER NOT NULL, `videoEnabled` INTEGER NOT NULL, `pixelfed` INTEGER NOT NULL, PRIMARY KEY(`uri`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "uri",
|
||||
"columnName": "uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "maxStatusChars",
|
||||
"columnName": "maxStatusChars",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "maxPhotoSize",
|
||||
"columnName": "maxPhotoSize",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "maxVideoSize",
|
||||
"columnName": "maxVideoSize",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "albumLimit",
|
||||
"columnName": "albumLimit",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "videoEnabled",
|
||||
"columnName": "videoEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pixelfed",
|
||||
"columnName": "pixelfed",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"uri"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "users",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `username` TEXT NOT NULL, `display_name` TEXT NOT NULL, `avatar_static` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accessToken` TEXT NOT NULL, `refreshToken` TEXT, `clientId` TEXT NOT NULL, `clientSecret` TEXT NOT NULL, PRIMARY KEY(`user_id`, `instance_uri`), FOREIGN KEY(`instance_uri`) REFERENCES `instances`(`uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "user_id",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance_uri",
|
||||
"columnName": "instance_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "username",
|
||||
"columnName": "username",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "display_name",
|
||||
"columnName": "display_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "avatar_static",
|
||||
"columnName": "avatar_static",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isActive",
|
||||
"columnName": "isActive",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accessToken",
|
||||
"columnName": "accessToken",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "refreshToken",
|
||||
"columnName": "refreshToken",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "clientId",
|
||||
"columnName": "clientId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "clientSecret",
|
||||
"columnName": "clientSecret",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_users_instance_uri",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"instance_uri"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_users_instance_uri` ON `${TABLE_NAME}` (`instance_uri`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "instances",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"instance_uri"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"uri"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "homePosts",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `id` TEXT NOT NULL, `uri` TEXT, `created_at` TEXT, `account` TEXT, `content` TEXT, `visibility` TEXT, `sensitive` INTEGER, `spoiler_text` TEXT, `media_attachments` TEXT, `application` TEXT, `mentions` TEXT, `tags` TEXT, `emojis` TEXT, `reblogs_count` INTEGER, `favourites_count` INTEGER, `replies_count` INTEGER, `url` TEXT, `in_reply_to_id` TEXT, `in_reply_to_account` TEXT, `reblog` TEXT, `poll` TEXT, `card` TEXT, `language` TEXT, `text` TEXT, `favourited` INTEGER, `reblogged` INTEGER, `muted` INTEGER, `bookmarked` INTEGER, `pinned` INTEGER, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "user_id",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance_uri",
|
||||
"columnName": "instance_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "uri",
|
||||
"columnName": "uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "created_at",
|
||||
"columnName": "created_at",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "account",
|
||||
"columnName": "account",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "content",
|
||||
"columnName": "content",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "visibility",
|
||||
"columnName": "visibility",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "sensitive",
|
||||
"columnName": "sensitive",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "spoiler_text",
|
||||
"columnName": "spoiler_text",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "media_attachments",
|
||||
"columnName": "media_attachments",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "application",
|
||||
"columnName": "application",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "mentions",
|
||||
"columnName": "mentions",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "tags",
|
||||
"columnName": "tags",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "emojis",
|
||||
"columnName": "emojis",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblogs_count",
|
||||
"columnName": "reblogs_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "favourites_count",
|
||||
"columnName": "favourites_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "replies_count",
|
||||
"columnName": "replies_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "in_reply_to_id",
|
||||
"columnName": "in_reply_to_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "in_reply_to_account",
|
||||
"columnName": "in_reply_to_account",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblog",
|
||||
"columnName": "reblog",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "poll",
|
||||
"columnName": "poll",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "card",
|
||||
"columnName": "card",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "language",
|
||||
"columnName": "language",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "text",
|
||||
"columnName": "text",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "favourited",
|
||||
"columnName": "favourited",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblogged",
|
||||
"columnName": "reblogged",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "muted",
|
||||
"columnName": "muted",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "bookmarked",
|
||||
"columnName": "bookmarked",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "pinned",
|
||||
"columnName": "pinned",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_homePosts_user_id_instance_uri",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_homePosts_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "publicPosts",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `id` TEXT NOT NULL, `uri` TEXT, `created_at` TEXT, `account` TEXT, `content` TEXT, `visibility` TEXT, `sensitive` INTEGER, `spoiler_text` TEXT, `media_attachments` TEXT, `application` TEXT, `mentions` TEXT, `tags` TEXT, `emojis` TEXT, `reblogs_count` INTEGER, `favourites_count` INTEGER, `replies_count` INTEGER, `url` TEXT, `in_reply_to_id` TEXT, `in_reply_to_account` TEXT, `reblog` TEXT, `poll` TEXT, `card` TEXT, `language` TEXT, `text` TEXT, `favourited` INTEGER, `reblogged` INTEGER, `muted` INTEGER, `bookmarked` INTEGER, `pinned` INTEGER, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "user_id",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance_uri",
|
||||
"columnName": "instance_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "uri",
|
||||
"columnName": "uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "created_at",
|
||||
"columnName": "created_at",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "account",
|
||||
"columnName": "account",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "content",
|
||||
"columnName": "content",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "visibility",
|
||||
"columnName": "visibility",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "sensitive",
|
||||
"columnName": "sensitive",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "spoiler_text",
|
||||
"columnName": "spoiler_text",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "media_attachments",
|
||||
"columnName": "media_attachments",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "application",
|
||||
"columnName": "application",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "mentions",
|
||||
"columnName": "mentions",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "tags",
|
||||
"columnName": "tags",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "emojis",
|
||||
"columnName": "emojis",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblogs_count",
|
||||
"columnName": "reblogs_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "favourites_count",
|
||||
"columnName": "favourites_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "replies_count",
|
||||
"columnName": "replies_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "in_reply_to_id",
|
||||
"columnName": "in_reply_to_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "in_reply_to_account",
|
||||
"columnName": "in_reply_to_account",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblog",
|
||||
"columnName": "reblog",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "poll",
|
||||
"columnName": "poll",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "card",
|
||||
"columnName": "card",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "language",
|
||||
"columnName": "language",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "text",
|
||||
"columnName": "text",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "favourited",
|
||||
"columnName": "favourited",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblogged",
|
||||
"columnName": "reblogged",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "muted",
|
||||
"columnName": "muted",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "bookmarked",
|
||||
"columnName": "bookmarked",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "pinned",
|
||||
"columnName": "pinned",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_publicPosts_user_id_instance_uri",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_publicPosts_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "notifications",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` TEXT, `created_at` TEXT, `account` TEXT, `status` TEXT, `user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "created_at",
|
||||
"columnName": "created_at",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "account",
|
||||
"columnName": "account",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "status",
|
||||
"columnName": "status",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "user_id",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance_uri",
|
||||
"columnName": "instance_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_notifications_user_id_instance_uri",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_notifications_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "tabsChecked",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`index` INTEGER NOT NULL, `user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `tab` TEXT NOT NULL, `checked` INTEGER NOT NULL, PRIMARY KEY(`index`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "index",
|
||||
"columnName": "index",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "user_id",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance_uri",
|
||||
"columnName": "instance_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tab",
|
||||
"columnName": "tab",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "checked",
|
||||
"columnName": "checked",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"index",
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_tabsChecked_user_id_instance_uri",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_tabsChecked_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"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, 'cd0310b10eff0e3e961b3cd7a8172b81')"
|
||||
]
|
||||
}
|
||||
}
|
@ -1,985 +0,0 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 8,
|
||||
"identityHash": "37b7f1d842d148e1d117ac9caae8fb51",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "instances",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uri` TEXT NOT NULL, `title` TEXT NOT NULL, `maxStatusChars` INTEGER NOT NULL, `maxPhotoSize` INTEGER NOT NULL, `maxVideoSize` INTEGER NOT NULL, `albumLimit` INTEGER NOT NULL, `videoEnabled` INTEGER NOT NULL, `pixelfed` INTEGER NOT NULL, PRIMARY KEY(`uri`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "uri",
|
||||
"columnName": "uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "maxStatusChars",
|
||||
"columnName": "maxStatusChars",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "maxPhotoSize",
|
||||
"columnName": "maxPhotoSize",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "maxVideoSize",
|
||||
"columnName": "maxVideoSize",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "albumLimit",
|
||||
"columnName": "albumLimit",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "videoEnabled",
|
||||
"columnName": "videoEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pixelfed",
|
||||
"columnName": "pixelfed",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"uri"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "users",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `username` TEXT NOT NULL, `display_name` TEXT NOT NULL, `avatar_static` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accessToken` TEXT NOT NULL, `refreshToken` TEXT, `clientId` TEXT NOT NULL, `clientSecret` TEXT NOT NULL, PRIMARY KEY(`user_id`, `instance_uri`), FOREIGN KEY(`instance_uri`) REFERENCES `instances`(`uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "user_id",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance_uri",
|
||||
"columnName": "instance_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "username",
|
||||
"columnName": "username",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "display_name",
|
||||
"columnName": "display_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "avatar_static",
|
||||
"columnName": "avatar_static",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isActive",
|
||||
"columnName": "isActive",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accessToken",
|
||||
"columnName": "accessToken",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "refreshToken",
|
||||
"columnName": "refreshToken",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "clientId",
|
||||
"columnName": "clientId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "clientSecret",
|
||||
"columnName": "clientSecret",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_users_instance_uri",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"instance_uri"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_users_instance_uri` ON `${TABLE_NAME}` (`instance_uri`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "instances",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"instance_uri"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"uri"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "homePosts",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `id` TEXT NOT NULL, `uri` TEXT, `created_at` TEXT, `account` TEXT, `content` TEXT, `visibility` TEXT, `sensitive` INTEGER, `spoiler_text` TEXT, `media_attachments` TEXT, `application` TEXT, `mentions` TEXT, `tags` TEXT, `emojis` TEXT, `reblogs_count` INTEGER, `favourites_count` INTEGER, `replies_count` INTEGER, `url` TEXT, `in_reply_to_id` TEXT, `in_reply_to_account` TEXT, `reblog` TEXT, `poll` TEXT, `card` TEXT, `language` TEXT, `text` TEXT, `favourited` INTEGER, `reblogged` INTEGER, `muted` INTEGER, `bookmarked` INTEGER, `pinned` INTEGER, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "user_id",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance_uri",
|
||||
"columnName": "instance_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "uri",
|
||||
"columnName": "uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "created_at",
|
||||
"columnName": "created_at",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "account",
|
||||
"columnName": "account",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "content",
|
||||
"columnName": "content",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "visibility",
|
||||
"columnName": "visibility",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "sensitive",
|
||||
"columnName": "sensitive",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "spoiler_text",
|
||||
"columnName": "spoiler_text",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "media_attachments",
|
||||
"columnName": "media_attachments",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "application",
|
||||
"columnName": "application",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "mentions",
|
||||
"columnName": "mentions",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "tags",
|
||||
"columnName": "tags",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "emojis",
|
||||
"columnName": "emojis",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblogs_count",
|
||||
"columnName": "reblogs_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "favourites_count",
|
||||
"columnName": "favourites_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "replies_count",
|
||||
"columnName": "replies_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "in_reply_to_id",
|
||||
"columnName": "in_reply_to_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "in_reply_to_account",
|
||||
"columnName": "in_reply_to_account",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblog",
|
||||
"columnName": "reblog",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "poll",
|
||||
"columnName": "poll",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "card",
|
||||
"columnName": "card",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "language",
|
||||
"columnName": "language",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "text",
|
||||
"columnName": "text",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "favourited",
|
||||
"columnName": "favourited",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblogged",
|
||||
"columnName": "reblogged",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "muted",
|
||||
"columnName": "muted",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "bookmarked",
|
||||
"columnName": "bookmarked",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "pinned",
|
||||
"columnName": "pinned",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_homePosts_user_id_instance_uri",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_homePosts_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "publicPosts",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `id` TEXT NOT NULL, `uri` TEXT, `created_at` TEXT, `account` TEXT, `content` TEXT, `visibility` TEXT, `sensitive` INTEGER, `spoiler_text` TEXT, `media_attachments` TEXT, `application` TEXT, `mentions` TEXT, `tags` TEXT, `emojis` TEXT, `reblogs_count` INTEGER, `favourites_count` INTEGER, `replies_count` INTEGER, `url` TEXT, `in_reply_to_id` TEXT, `in_reply_to_account` TEXT, `reblog` TEXT, `poll` TEXT, `card` TEXT, `language` TEXT, `text` TEXT, `favourited` INTEGER, `reblogged` INTEGER, `muted` INTEGER, `bookmarked` INTEGER, `pinned` INTEGER, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "user_id",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance_uri",
|
||||
"columnName": "instance_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "uri",
|
||||
"columnName": "uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "created_at",
|
||||
"columnName": "created_at",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "account",
|
||||
"columnName": "account",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "content",
|
||||
"columnName": "content",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "visibility",
|
||||
"columnName": "visibility",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "sensitive",
|
||||
"columnName": "sensitive",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "spoiler_text",
|
||||
"columnName": "spoiler_text",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "media_attachments",
|
||||
"columnName": "media_attachments",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "application",
|
||||
"columnName": "application",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "mentions",
|
||||
"columnName": "mentions",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "tags",
|
||||
"columnName": "tags",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "emojis",
|
||||
"columnName": "emojis",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblogs_count",
|
||||
"columnName": "reblogs_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "favourites_count",
|
||||
"columnName": "favourites_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "replies_count",
|
||||
"columnName": "replies_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "in_reply_to_id",
|
||||
"columnName": "in_reply_to_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "in_reply_to_account",
|
||||
"columnName": "in_reply_to_account",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblog",
|
||||
"columnName": "reblog",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "poll",
|
||||
"columnName": "poll",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "card",
|
||||
"columnName": "card",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "language",
|
||||
"columnName": "language",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "text",
|
||||
"columnName": "text",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "favourited",
|
||||
"columnName": "favourited",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblogged",
|
||||
"columnName": "reblogged",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "muted",
|
||||
"columnName": "muted",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "bookmarked",
|
||||
"columnName": "bookmarked",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "pinned",
|
||||
"columnName": "pinned",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_publicPosts_user_id_instance_uri",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_publicPosts_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "notifications",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` TEXT, `created_at` TEXT, `account` TEXT, `status` TEXT, `user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "created_at",
|
||||
"columnName": "created_at",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "account",
|
||||
"columnName": "account",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "status",
|
||||
"columnName": "status",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "user_id",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance_uri",
|
||||
"columnName": "instance_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_notifications_user_id_instance_uri",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_notifications_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "tabsChecked",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`index` INTEGER NOT NULL, `user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `tab` TEXT NOT NULL, `checked` INTEGER NOT NULL, PRIMARY KEY(`index`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "index",
|
||||
"columnName": "index",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "user_id",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance_uri",
|
||||
"columnName": "instance_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tab",
|
||||
"columnName": "tab",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "checked",
|
||||
"columnName": "checked",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"index",
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_tabsChecked_user_id_instance_uri",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_tabsChecked_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "directMessages",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `unread` INTEGER, `accounts` TEXT, `last_status` TEXT, `user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "unread",
|
||||
"columnName": "unread",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "accounts",
|
||||
"columnName": "accounts",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "last_status",
|
||||
"columnName": "last_status",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "user_id",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance_uri",
|
||||
"columnName": "instance_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_directMessages_user_id_instance_uri",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_directMessages_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "directMessagesThreads",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `hidden` INTEGER, `isAuthor` INTEGER, `type` TEXT, `text` TEXT, `media` TEXT, `carousel` TEXT, `created_at` TEXT, `timeAgo` TEXT, `reportId` TEXT, `conversationsId` TEXT NOT NULL, `user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, PRIMARY KEY(`id`, `conversationsId`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "hidden",
|
||||
"columnName": "hidden",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAuthor",
|
||||
"columnName": "isAuthor",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "text",
|
||||
"columnName": "text",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "media",
|
||||
"columnName": "media",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "carousel",
|
||||
"columnName": "carousel",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "created_at",
|
||||
"columnName": "created_at",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "timeAgo",
|
||||
"columnName": "timeAgo",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reportId",
|
||||
"columnName": "reportId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "conversationsId",
|
||||
"columnName": "conversationsId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "user_id",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance_uri",
|
||||
"columnName": "instance_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"conversationsId",
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_directMessagesThreads_user_id_instance_uri_conversationsId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id",
|
||||
"instance_uri",
|
||||
"conversationsId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_directMessagesThreads_user_id_instance_uri_conversationsId` ON `${TABLE_NAME}` (`user_id`, `instance_uri`, `conversationsId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"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, '37b7f1d842d148e1d117ac9caae8fb51')"
|
||||
]
|
||||
}
|
||||
}
|
@ -1,991 +0,0 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 9,
|
||||
"identityHash": "5ee0e8fbaef28650cbea6670e24e08bb",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "instances",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uri` TEXT NOT NULL, `title` TEXT NOT NULL, `maxStatusChars` INTEGER NOT NULL, `maxPhotoSize` INTEGER NOT NULL, `maxVideoSize` INTEGER NOT NULL, `albumLimit` INTEGER NOT NULL, `videoEnabled` INTEGER NOT NULL, `pixelfed` INTEGER NOT NULL, PRIMARY KEY(`uri`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "uri",
|
||||
"columnName": "uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "maxStatusChars",
|
||||
"columnName": "maxStatusChars",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "maxPhotoSize",
|
||||
"columnName": "maxPhotoSize",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "maxVideoSize",
|
||||
"columnName": "maxVideoSize",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "albumLimit",
|
||||
"columnName": "albumLimit",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "videoEnabled",
|
||||
"columnName": "videoEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pixelfed",
|
||||
"columnName": "pixelfed",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"uri"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "users",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `username` TEXT NOT NULL, `display_name` TEXT NOT NULL, `avatar_static` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accessToken` TEXT NOT NULL, `refreshToken` TEXT, `clientId` TEXT NOT NULL, `clientSecret` TEXT NOT NULL, PRIMARY KEY(`user_id`, `instance_uri`), FOREIGN KEY(`instance_uri`) REFERENCES `instances`(`uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "user_id",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance_uri",
|
||||
"columnName": "instance_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "username",
|
||||
"columnName": "username",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "display_name",
|
||||
"columnName": "display_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "avatar_static",
|
||||
"columnName": "avatar_static",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isActive",
|
||||
"columnName": "isActive",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accessToken",
|
||||
"columnName": "accessToken",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "refreshToken",
|
||||
"columnName": "refreshToken",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "clientId",
|
||||
"columnName": "clientId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "clientSecret",
|
||||
"columnName": "clientSecret",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_users_instance_uri",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"instance_uri"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_users_instance_uri` ON `${TABLE_NAME}` (`instance_uri`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "instances",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"instance_uri"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"uri"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "homePosts",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `id` TEXT NOT NULL, `uri` TEXT, `created_at` TEXT, `account` TEXT, `content` TEXT, `visibility` TEXT, `sensitive` INTEGER, `spoiler_text` TEXT, `media_attachments` TEXT, `application` TEXT, `mentions` TEXT, `tags` TEXT, `emojis` TEXT, `reblogs_count` INTEGER, `favourites_count` INTEGER, `replies_count` INTEGER, `url` TEXT, `in_reply_to_id` TEXT, `in_reply_to_account` TEXT, `reblog` TEXT, `poll` TEXT, `card` TEXT, `language` TEXT, `text` TEXT, `favourited` INTEGER, `reblogged` INTEGER, `muted` INTEGER, `bookmarked` INTEGER, `pinned` INTEGER, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "user_id",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance_uri",
|
||||
"columnName": "instance_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "uri",
|
||||
"columnName": "uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "created_at",
|
||||
"columnName": "created_at",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "account",
|
||||
"columnName": "account",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "content",
|
||||
"columnName": "content",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "visibility",
|
||||
"columnName": "visibility",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "sensitive",
|
||||
"columnName": "sensitive",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "spoiler_text",
|
||||
"columnName": "spoiler_text",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "media_attachments",
|
||||
"columnName": "media_attachments",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "application",
|
||||
"columnName": "application",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "mentions",
|
||||
"columnName": "mentions",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "tags",
|
||||
"columnName": "tags",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "emojis",
|
||||
"columnName": "emojis",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblogs_count",
|
||||
"columnName": "reblogs_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "favourites_count",
|
||||
"columnName": "favourites_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "replies_count",
|
||||
"columnName": "replies_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "in_reply_to_id",
|
||||
"columnName": "in_reply_to_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "in_reply_to_account",
|
||||
"columnName": "in_reply_to_account",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblog",
|
||||
"columnName": "reblog",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "poll",
|
||||
"columnName": "poll",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "card",
|
||||
"columnName": "card",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "language",
|
||||
"columnName": "language",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "text",
|
||||
"columnName": "text",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "favourited",
|
||||
"columnName": "favourited",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblogged",
|
||||
"columnName": "reblogged",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "muted",
|
||||
"columnName": "muted",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "bookmarked",
|
||||
"columnName": "bookmarked",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "pinned",
|
||||
"columnName": "pinned",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_homePosts_user_id_instance_uri",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_homePosts_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "publicPosts",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `id` TEXT NOT NULL, `uri` TEXT, `created_at` TEXT, `account` TEXT, `content` TEXT, `visibility` TEXT, `sensitive` INTEGER, `spoiler_text` TEXT, `media_attachments` TEXT, `application` TEXT, `mentions` TEXT, `tags` TEXT, `emojis` TEXT, `reblogs_count` INTEGER, `favourites_count` INTEGER, `replies_count` INTEGER, `url` TEXT, `in_reply_to_id` TEXT, `in_reply_to_account` TEXT, `reblog` TEXT, `poll` TEXT, `card` TEXT, `language` TEXT, `text` TEXT, `favourited` INTEGER, `reblogged` INTEGER, `muted` INTEGER, `bookmarked` INTEGER, `pinned` INTEGER, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "user_id",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance_uri",
|
||||
"columnName": "instance_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "uri",
|
||||
"columnName": "uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "created_at",
|
||||
"columnName": "created_at",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "account",
|
||||
"columnName": "account",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "content",
|
||||
"columnName": "content",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "visibility",
|
||||
"columnName": "visibility",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "sensitive",
|
||||
"columnName": "sensitive",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "spoiler_text",
|
||||
"columnName": "spoiler_text",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "media_attachments",
|
||||
"columnName": "media_attachments",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "application",
|
||||
"columnName": "application",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "mentions",
|
||||
"columnName": "mentions",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "tags",
|
||||
"columnName": "tags",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "emojis",
|
||||
"columnName": "emojis",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblogs_count",
|
||||
"columnName": "reblogs_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "favourites_count",
|
||||
"columnName": "favourites_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "replies_count",
|
||||
"columnName": "replies_count",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "in_reply_to_id",
|
||||
"columnName": "in_reply_to_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "in_reply_to_account",
|
||||
"columnName": "in_reply_to_account",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblog",
|
||||
"columnName": "reblog",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "poll",
|
||||
"columnName": "poll",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "card",
|
||||
"columnName": "card",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "language",
|
||||
"columnName": "language",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "text",
|
||||
"columnName": "text",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "favourited",
|
||||
"columnName": "favourited",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblogged",
|
||||
"columnName": "reblogged",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "muted",
|
||||
"columnName": "muted",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "bookmarked",
|
||||
"columnName": "bookmarked",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "pinned",
|
||||
"columnName": "pinned",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_publicPosts_user_id_instance_uri",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_publicPosts_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "notifications",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` TEXT, `created_at` TEXT, `account` TEXT, `status` TEXT, `user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "created_at",
|
||||
"columnName": "created_at",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "account",
|
||||
"columnName": "account",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "status",
|
||||
"columnName": "status",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "user_id",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance_uri",
|
||||
"columnName": "instance_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_notifications_user_id_instance_uri",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_notifications_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "tabsChecked",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`index` INTEGER NOT NULL, `user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, `tab` TEXT NOT NULL, `checked` INTEGER NOT NULL, `filter` TEXT, PRIMARY KEY(`index`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "index",
|
||||
"columnName": "index",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "user_id",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance_uri",
|
||||
"columnName": "instance_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tab",
|
||||
"columnName": "tab",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "checked",
|
||||
"columnName": "checked",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "filter",
|
||||
"columnName": "filter",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"index",
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_tabsChecked_user_id_instance_uri",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_tabsChecked_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "directMessages",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `unread` INTEGER, `accounts` TEXT, `last_status` TEXT, `user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, PRIMARY KEY(`id`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "unread",
|
||||
"columnName": "unread",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "accounts",
|
||||
"columnName": "accounts",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "last_status",
|
||||
"columnName": "last_status",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "user_id",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance_uri",
|
||||
"columnName": "instance_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_directMessages_user_id_instance_uri",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_directMessages_user_id_instance_uri` ON `${TABLE_NAME}` (`user_id`, `instance_uri`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "directMessagesThreads",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `hidden` INTEGER, `isAuthor` INTEGER, `type` TEXT, `text` TEXT, `media` TEXT, `carousel` TEXT, `created_at` TEXT, `timeAgo` TEXT, `reportId` TEXT, `conversationsId` TEXT NOT NULL, `user_id` TEXT NOT NULL, `instance_uri` TEXT NOT NULL, PRIMARY KEY(`id`, `conversationsId`, `user_id`, `instance_uri`), FOREIGN KEY(`user_id`, `instance_uri`) REFERENCES `users`(`user_id`, `instance_uri`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "hidden",
|
||||
"columnName": "hidden",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "isAuthor",
|
||||
"columnName": "isAuthor",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "text",
|
||||
"columnName": "text",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "media",
|
||||
"columnName": "media",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "carousel",
|
||||
"columnName": "carousel",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "created_at",
|
||||
"columnName": "created_at",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "timeAgo",
|
||||
"columnName": "timeAgo",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reportId",
|
||||
"columnName": "reportId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "conversationsId",
|
||||
"columnName": "conversationsId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "user_id",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance_uri",
|
||||
"columnName": "instance_uri",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"conversationsId",
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_directMessagesThreads_user_id_instance_uri_conversationsId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"user_id",
|
||||
"instance_uri",
|
||||
"conversationsId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_directMessagesThreads_user_id_instance_uri_conversationsId` ON `${TABLE_NAME}` (`user_id`, `instance_uri`, `conversationsId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"user_id",
|
||||
"instance_uri"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"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, '5ee0e8fbaef28650cbea6670e24e08bb')"
|
||||
]
|
||||
}
|
||||
}
|
@ -23,9 +23,14 @@
|
||||
android:name=".utils.PixelDroidApplication"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/BaseAppTheme">
|
||||
<activity
|
||||
android:name=".directMessages.ConversationsActivity"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
android:enabled="false"
|
||||
@ -38,22 +43,32 @@
|
||||
<activity
|
||||
android:name=".posts.AlbumActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/TransparentAlbumActivity"/>
|
||||
android:theme="@style/AppTheme.ActionBar.Transparent" />
|
||||
<activity
|
||||
android:name=".profile.EditProfileActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/BaseAppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".posts.MediaViewerActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:exported="false"
|
||||
android:theme="@style/BaseAppTheme" />
|
||||
<activity android:name=".postCreation.camera.CameraActivity"
|
||||
android:theme="@style/BaseAppTheme"/>
|
||||
android:theme="@style/BaseAppTheme.NoActionBar" />
|
||||
<activity android:name=".postCreation.camera.CameraActivity" />
|
||||
<activity
|
||||
android:name=".postCreation.camera.CameraActivityShortcut"
|
||||
android:exported="true"
|
||||
android:parentActivityName=".MainActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".MainActivity" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".posts.ReportActivity"
|
||||
android:theme="@style/BaseAppTheme" />
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/BaseAppTheme"
|
||||
tools:ignore="LockedOrientationActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".stories.StoriesActivity" />
|
||||
@ -74,29 +89,39 @@
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".profile.FollowsActivity"
|
||||
android:theme="@style/BaseAppTheme" />
|
||||
android:theme="@style/BaseAppTheme"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
tools:ignore="LockedOrientationActivity" />
|
||||
<activity
|
||||
android:name=".posts.feeds.uncachedFeeds.hashtags.HashTagActivity"
|
||||
android:theme="@style/BaseAppTheme" />
|
||||
android:theme="@style/BaseAppTheme"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
tools:ignore="LockedOrientationActivity" />
|
||||
<activity
|
||||
android:name=".posts.PostActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
tools:ignore="LockedOrientationActivity"
|
||||
android:theme="@style/BaseAppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".profile.ProfileActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
tools:ignore="LockedOrientationActivity"
|
||||
android:theme="@style/BaseAppTheme"/>
|
||||
<activity android:name=".profile.CollectionActivity"
|
||||
android:theme="@style/BaseAppTheme"/>
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
android:label="@string/title_activity_settings2"
|
||||
android:parentActivityName=".main.MainActivity"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:theme="@style/BaseAppTheme" />
|
||||
<activity
|
||||
android:name=".main.MainActivity"
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.App.Starting"
|
||||
android:windowSoftInputMode="adjustPan">
|
||||
android:windowSoftInputMode="adjustPan"
|
||||
tools:ignore="LockedOrientationActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@ -106,14 +131,17 @@
|
||||
<meta-data
|
||||
android:name="android.app.default_searchable"
|
||||
android:value="org.pixeldroid.app.searchDiscover.SearchActivity" />
|
||||
<meta-data android:name="android.app.shortcuts"
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".login.LoginActivity"
|
||||
android:name=".LoginActivity"
|
||||
android:exported="true"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/BaseAppTheme"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
tools:ignore="LockedOrientationActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
@ -129,7 +157,9 @@
|
||||
android:name=".searchDiscover.SearchActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/BaseAppTheme"
|
||||
android:launchMode="singleTop">
|
||||
android:launchMode="singleTop"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
tools:ignore="LockedOrientationActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEARCH" />
|
||||
</intent-filter>
|
||||
@ -141,12 +171,6 @@
|
||||
<activity android:name=".searchDiscover.TrendingActivity"
|
||||
android:theme="@style/BaseAppTheme" />
|
||||
|
||||
<activity android:name=".directmessages.ConversationActivity"
|
||||
android:theme="@style/BaseAppTheme"
|
||||
android:windowSoftInputMode="adjustPan" />
|
||||
<activity android:name="org.pixeldroid.app.directmessages.DirectMessagesActivity"
|
||||
android:theme="@style/BaseAppTheme" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
@ -156,16 +180,6 @@
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
android:exported="false"
|
||||
tools:node="merge">
|
||||
<meta-data
|
||||
android:name="androidx.work.WorkManagerInitializer"
|
||||
android:value="androidx.startup"
|
||||
tools:node="remove" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
345
app/src/main/java/org/pixeldroid/app/LoginActivity.kt
Normal file
345
app/src/main/java/org/pixeldroid/app/LoginActivity.kt
Normal file
@ -0,0 +1,345 @@
|
||||
package org.pixeldroid.app
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.databinding.ActivityLoginBinding
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.api.PixelfedAPI
|
||||
import org.pixeldroid.app.utils.api.objects.Application
|
||||
import org.pixeldroid.app.utils.api.objects.Instance
|
||||
import org.pixeldroid.app.utils.api.objects.NodeInfo
|
||||
import org.pixeldroid.app.utils.db.addUser
|
||||
import org.pixeldroid.app.utils.db.storeInstance
|
||||
import org.pixeldroid.app.utils.hasInternet
|
||||
import org.pixeldroid.app.utils.normalizeDomain
|
||||
import org.pixeldroid.app.utils.notificationsWorker.makeChannelGroupId
|
||||
import org.pixeldroid.app.utils.notificationsWorker.makeNotificationChannels
|
||||
import org.pixeldroid.app.utils.openUrl
|
||||
import org.pixeldroid.app.utils.validDomain
|
||||
|
||||
/**
|
||||
Overview of the flow of the login process: (boxes are requests done in parallel,
|
||||
since they do not depend on each other)
|
||||
|
||||
_________________________________
|
||||
|[PixelfedAPI.registerApplication]|
|
||||
|[PixelfedAPI.wellKnownNodeInfo] |
|
||||
̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅
|
||||
+----> [PixelfedAPI.nodeInfoSchema] (and then [PixelfedAPI.instance] if needed)
|
||||
+----> [promptOAuth]
|
||||
+----> [PixelfedAPI.obtainToken]
|
||||
+----> [PixelfedAPI.verifyCredentials]
|
||||
|
||||
*/
|
||||
|
||||
class LoginActivity : BaseActivity() {
|
||||
|
||||
companion object {
|
||||
private const val PACKAGE_ID = BuildConfig.APPLICATION_ID
|
||||
private const val PREFERENCE_NAME = "$PACKAGE_ID.loginPref"
|
||||
private const val SCOPE = "read write follow"
|
||||
}
|
||||
|
||||
private lateinit var oauthScheme: String
|
||||
private lateinit var appName: String
|
||||
private lateinit var preferences: SharedPreferences
|
||||
|
||||
private lateinit var pixelfedAPI: PixelfedAPI
|
||||
private var inputVisibility: Int = View.GONE
|
||||
|
||||
private lateinit var binding: ActivityLoginBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityLoginBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
loadingAnimation(true)
|
||||
appName = getString(R.string.app_name)
|
||||
oauthScheme = getString(R.string.auth_scheme)
|
||||
preferences = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
if (hasInternet(applicationContext)) {
|
||||
binding.connectInstanceButton.setOnClickListener {
|
||||
registerAppToServer(normalizeDomain(binding.editText.text.toString()))
|
||||
}
|
||||
binding.whatsAnInstanceTextView.setOnClickListener{ whatsAnInstance() }
|
||||
inputVisibility = View.VISIBLE
|
||||
} else {
|
||||
binding.loginActivityConnectionRequired.visibility = View.VISIBLE
|
||||
binding.loginActivityConnectionRequiredButton.setOnClickListener {
|
||||
finish()
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
loadingAnimation(false)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
val url: Uri? = intent.data
|
||||
|
||||
//Check if the activity was started after the authentication
|
||||
if (url == null || !url.toString().startsWith("$oauthScheme://$PACKAGE_ID")) return
|
||||
loadingAnimation(true)
|
||||
|
||||
val code = url.getQueryParameter("code")
|
||||
authenticate(code)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
loadingAnimation(false)
|
||||
}
|
||||
|
||||
|
||||
private fun whatsAnInstance() {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setView(layoutInflater.inflate(R.layout.whats_an_instance_explanation, null))
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
// Create the AlertDialog
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun hideKeyboard() {
|
||||
val view = currentFocus
|
||||
if (view != null) {
|
||||
(getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).hideSoftInputFromWindow(
|
||||
view.windowToken,
|
||||
InputMethodManager.HIDE_NOT_ALWAYS
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerAppToServer(normalizedDomain: String) {
|
||||
|
||||
if(!validDomain(normalizedDomain)) return failedRegistration(getString(R.string.invalid_domain))
|
||||
|
||||
hideKeyboard()
|
||||
loadingAnimation(true)
|
||||
|
||||
pixelfedAPI = PixelfedAPI.createFromUrl(normalizedDomain)
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val credentialsDeferred: Deferred<Application?> = async {
|
||||
try {
|
||||
pixelfedAPI.registerApplication(
|
||||
appName, "$oauthScheme://$PACKAGE_ID", SCOPE, "https://pixeldroid.org"
|
||||
)
|
||||
} catch (exception: Exception) {
|
||||
return@async null
|
||||
}
|
||||
}
|
||||
|
||||
val nodeInfoJRD = pixelfedAPI.wellKnownNodeInfo()
|
||||
|
||||
val credentials = credentialsDeferred.await()
|
||||
|
||||
val clientId = credentials?.client_id ?: return@launch failedRegistration()
|
||||
preferences.edit()
|
||||
.putString("clientID", clientId)
|
||||
.putString("clientSecret", credentials.client_secret)
|
||||
.apply()
|
||||
|
||||
|
||||
// c.f. https://nodeinfo.diaspora.software/protocol.html for more info
|
||||
val nodeInfoSchemaUrl = nodeInfoJRD.links.firstOrNull {
|
||||
it.rel == "http://nodeinfo.diaspora.software/ns/schema/2.0"
|
||||
}?.href ?: return@launch failedRegistration(getString(R.string.instance_error))
|
||||
|
||||
nodeInfoSchema(normalizedDomain, clientId, nodeInfoSchemaUrl)
|
||||
} catch (exception: Exception) {
|
||||
return@launch failedRegistration()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun nodeInfoSchema(
|
||||
normalizedDomain: String,
|
||||
clientId: String,
|
||||
nodeInfoSchemaUrl: String
|
||||
) = coroutineScope {
|
||||
|
||||
val nodeInfo: NodeInfo = try {
|
||||
pixelfedAPI.nodeInfoSchema(nodeInfoSchemaUrl)
|
||||
} catch (exception: Exception) {
|
||||
return@coroutineScope failedRegistration(getString(R.string.instance_error))
|
||||
}
|
||||
val domain: String = try {
|
||||
if (nodeInfo.hasInstanceEndpointInfo()) {
|
||||
preferences.edit().putString("nodeInfo", Gson().toJson(nodeInfo)).remove("instance").apply()
|
||||
nodeInfo.metadata?.config?.site?.url
|
||||
} else {
|
||||
val instance: Instance = try {
|
||||
pixelfedAPI.instance()
|
||||
} catch (exception: Exception) {
|
||||
return@coroutineScope failedRegistration(getString(R.string.instance_error))
|
||||
}
|
||||
preferences.edit().putString("instance", Gson().toJson(instance)).remove("nodeInfo").apply()
|
||||
instance.uri
|
||||
}
|
||||
} catch (e: IllegalArgumentException){ null }
|
||||
?: return@coroutineScope failedRegistration(getString(R.string.instance_error))
|
||||
|
||||
preferences.edit()
|
||||
.putString("domain", normalizeDomain(domain))
|
||||
.apply()
|
||||
|
||||
|
||||
if (!nodeInfo.software?.name.orEmpty().contains("pixelfed")) {
|
||||
MaterialAlertDialogBuilder(this@LoginActivity).apply {
|
||||
setMessage(R.string.instance_not_pixelfed_warning)
|
||||
setPositiveButton(R.string.instance_not_pixelfed_continue) { _, _ ->
|
||||
promptOAuth(normalizedDomain, clientId)
|
||||
}
|
||||
setNegativeButton(R.string.instance_not_pixelfed_cancel) { _, _ ->
|
||||
loadingAnimation(false)
|
||||
wipeSharedSettings()
|
||||
}
|
||||
}.show()
|
||||
} else if (nodeInfo.metadata?.config?.features?.mobile_apis != true) {
|
||||
MaterialAlertDialogBuilder(this@LoginActivity).apply {
|
||||
setMessage(R.string.api_not_enabled_dialog)
|
||||
setNegativeButton(android.R.string.ok) { _, _ ->
|
||||
loadingAnimation(false)
|
||||
wipeSharedSettings()
|
||||
}
|
||||
}.show()
|
||||
} else {
|
||||
promptOAuth(normalizedDomain, clientId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun promptOAuth(normalizedDomain: String, client_id: String) {
|
||||
|
||||
val url = "$normalizedDomain/oauth/authorize?" +
|
||||
"client_id" + "=" + client_id + "&" +
|
||||
"redirect_uri" + "=" + "$oauthScheme://$PACKAGE_ID" + "&" +
|
||||
"response_type=code" + "&" +
|
||||
"scope=${SCOPE.replace(" ", "%20")}"
|
||||
|
||||
if (!openUrl(url)) return failedRegistration(getString(R.string.browser_launch_failed))
|
||||
}
|
||||
|
||||
private fun authenticate(code: String?) {
|
||||
|
||||
// Get previous values from preferences
|
||||
val domain = preferences.getString("domain", "") as String
|
||||
val clientId = preferences.getString("clientID", "") as String
|
||||
val clientSecret = preferences.getString("clientSecret", "") as String
|
||||
|
||||
if (code.isNullOrBlank() || domain.isBlank() || clientId.isBlank() || clientSecret.isBlank()) {
|
||||
return failedRegistration(getString(R.string.auth_failed))
|
||||
}
|
||||
|
||||
//Successful authorization
|
||||
pixelfedAPI = PixelfedAPI.createFromUrl(domain)
|
||||
val gson = Gson()
|
||||
val nodeInfo: NodeInfo? = gson.fromJson(preferences.getString("nodeInfo", null), NodeInfo::class.java)
|
||||
val instance: Instance? = gson.fromJson(preferences.getString("instance", null), Instance::class.java)
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val token = pixelfedAPI.obtainToken(
|
||||
clientId, clientSecret, "$oauthScheme://$PACKAGE_ID", SCOPE, code,
|
||||
"authorization_code"
|
||||
)
|
||||
if (token.access_token == null) {
|
||||
return@launch failedRegistration(getString(R.string.token_error))
|
||||
}
|
||||
storeInstance(db, nodeInfo, instance)
|
||||
storeUser(
|
||||
token.access_token,
|
||||
token.refresh_token,
|
||||
clientId,
|
||||
clientSecret,
|
||||
domain
|
||||
)
|
||||
wipeSharedSettings()
|
||||
} catch (exception: Exception) {
|
||||
return@launch failedRegistration(getString(R.string.token_error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun failedRegistration(message: String = getString(R.string.registration_failed)) {
|
||||
loadingAnimation(false)
|
||||
binding.editText.error = message
|
||||
wipeSharedSettings()
|
||||
}
|
||||
|
||||
private fun wipeSharedSettings(){
|
||||
preferences.edit().clear().apply()
|
||||
}
|
||||
|
||||
private fun loadingAnimation(on: Boolean){
|
||||
if(on) {
|
||||
binding.loginActivityInstanceInputLayout.visibility = View.GONE
|
||||
binding.progressLayout.visibility = View.VISIBLE
|
||||
}
|
||||
else {
|
||||
binding.loginActivityInstanceInputLayout.visibility = inputVisibility
|
||||
binding.progressLayout.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun storeUser(accessToken: String, refreshToken: String?, clientId: String, clientSecret: String, instance: String) {
|
||||
try {
|
||||
val user = pixelfedAPI.verifyCredentials("Bearer $accessToken")
|
||||
db.userDao().deActivateActiveUsers()
|
||||
addUser(
|
||||
db,
|
||||
user,
|
||||
instance,
|
||||
activeUser = true,
|
||||
accessToken = accessToken,
|
||||
refreshToken = refreshToken,
|
||||
clientId = clientId,
|
||||
clientSecret = clientSecret
|
||||
)
|
||||
apiHolder.setToCurrentUser()
|
||||
} catch (exception: Exception) {
|
||||
return failedRegistration(getString(R.string.verify_credentials))
|
||||
}
|
||||
|
||||
fetchNotifications()
|
||||
val intent = Intent(this@LoginActivity, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
// Fetch the latest notifications of this account, to avoid launching old notifications
|
||||
private suspend fun fetchNotifications() {
|
||||
val user = db.userDao().getActiveUser()!!
|
||||
try {
|
||||
val notifications = apiHolder.api!!.notifications()
|
||||
|
||||
notifications.forEach{it.user_id = user.user_id; it.instance_uri = user.instance_uri}
|
||||
|
||||
db.notificationDao().insertAll(notifications)
|
||||
} catch (exception: Exception) {
|
||||
return failedRegistration(getString(R.string.login_notifications))
|
||||
}
|
||||
|
||||
makeNotificationChannels(
|
||||
applicationContext,
|
||||
user.fullHandle,
|
||||
makeChannelGroupId(user)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
499
app/src/main/java/org/pixeldroid/app/MainActivity.kt
Normal file
499
app/src/main/java/org/pixeldroid/app/MainActivity.kt
Normal file
@ -0,0 +1,499 @@
|
||||
package org.pixeldroid.app
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||
import com.mikepenz.materialdrawer.iconics.iconicsIcon
|
||||
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.ProfileDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.interfaces.*
|
||||
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
|
||||
import com.mikepenz.materialdrawer.util.DrawerImageLoader
|
||||
import com.mikepenz.materialdrawer.widget.AccountHeaderView
|
||||
import kotlinx.coroutines.launch
|
||||
import org.ligi.tracedroid.sending.sendTraceDroidStackTracesIfExist
|
||||
import org.pixeldroid.app.databinding.ActivityMainBinding
|
||||
import org.pixeldroid.app.directMessages.ConversationsActivity
|
||||
import org.pixeldroid.app.postCreation.camera.CameraFragment
|
||||
import org.pixeldroid.app.posts.NestedScrollableHost
|
||||
import org.pixeldroid.app.posts.feeds.cachedFeeds.CachedFeedFragment
|
||||
import org.pixeldroid.app.posts.feeds.cachedFeeds.notifications.NotificationsFragment
|
||||
import org.pixeldroid.app.posts.feeds.cachedFeeds.postFeeds.PostFeedFragment
|
||||
import org.pixeldroid.app.profile.ProfileActivity
|
||||
import org.pixeldroid.app.searchDiscover.SearchDiscoverFragment
|
||||
import org.pixeldroid.app.settings.SettingsActivity
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.api.objects.Notification
|
||||
import org.pixeldroid.app.utils.db.addUser
|
||||
import org.pixeldroid.app.utils.db.entities.HomeStatusDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.entities.PublicFeedStatusDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import org.pixeldroid.app.utils.hasInternet
|
||||
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.INSTANCE_NOTIFICATION_TAG
|
||||
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.SHOW_NOTIFICATION_TAG
|
||||
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.USER_NOTIFICATION_TAG
|
||||
import org.pixeldroid.app.utils.notificationsWorker.enablePullNotifications
|
||||
import org.pixeldroid.app.utils.notificationsWorker.removeNotificationChannelsFromAccount
|
||||
import java.time.Instant
|
||||
|
||||
|
||||
class MainActivity : BaseActivity() {
|
||||
|
||||
private lateinit var header: AccountHeaderView
|
||||
private var user: UserDatabaseEntity? = null
|
||||
|
||||
companion object {
|
||||
const val ADD_ACCOUNT_IDENTIFIER: Long = -13
|
||||
}
|
||||
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
installSplashScreen().setOnExitAnimationListener {
|
||||
it.remove()
|
||||
}
|
||||
|
||||
// Workaround for dynamic colors not applying due to splash screen?
|
||||
DynamicColors.applyToActivityIfAvailable(this)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
//get the currently active user
|
||||
user = db.userDao().getActiveUser()
|
||||
|
||||
if (notificationFromOtherUser()) return
|
||||
|
||||
//Check if we have logged in and gotten an access token
|
||||
if (user == null) {
|
||||
finish()
|
||||
launchActivity(LoginActivity(), firstTime = true)
|
||||
} else {
|
||||
sendTraceDroidStackTracesIfExist("contact@pixeldroid.org", this)
|
||||
|
||||
setupDrawer()
|
||||
val tabs: List<() -> Fragment> = listOf(
|
||||
{
|
||||
PostFeedFragment<HomeStatusDatabaseEntity>()
|
||||
.apply {
|
||||
arguments = Bundle().apply { putBoolean("home", true) }
|
||||
}
|
||||
},
|
||||
{ SearchDiscoverFragment() },
|
||||
{ CameraFragment() },
|
||||
{ NotificationsFragment() },
|
||||
{
|
||||
PostFeedFragment<PublicFeedStatusDatabaseEntity>()
|
||||
.apply {
|
||||
arguments = Bundle().apply { putBoolean("home", false) }
|
||||
}
|
||||
}
|
||||
)
|
||||
setupTabs(tabs)
|
||||
|
||||
val showNotification: Boolean = intent.getBooleanExtra(SHOW_NOTIFICATION_TAG, false)
|
||||
|
||||
if(showNotification){
|
||||
binding.viewPager.currentItem = 3
|
||||
}
|
||||
if (ActivityCompat.checkSelfPermission(applicationContext,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
) enablePullNotifications(this)
|
||||
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val notificationPermissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) { isGranted: Boolean ->
|
||||
if (isGranted) enablePullNotifications(this)
|
||||
}
|
||||
|
||||
// Checks if the activity was launched from a notification from another account than the
|
||||
// current active one, and if so switches to that account
|
||||
private fun notificationFromOtherUser(): Boolean {
|
||||
val userOfNotification: String? = intent.extras?.getString(USER_NOTIFICATION_TAG)
|
||||
val instanceOfNotification: String? = intent.extras?.getString(INSTANCE_NOTIFICATION_TAG)
|
||||
if (userOfNotification != null && instanceOfNotification != null
|
||||
&& (userOfNotification != user?.user_id
|
||||
|| instanceOfNotification != user?.instance_uri)
|
||||
) {
|
||||
|
||||
switchUser(userOfNotification, instanceOfNotification)
|
||||
|
||||
val newIntent = Intent(this, MainActivity::class.java)
|
||||
newIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
|
||||
if (intent.getBooleanExtra(SHOW_NOTIFICATION_TAG, false)) {
|
||||
newIntent.putExtra(SHOW_NOTIFICATION_TAG, true)
|
||||
}
|
||||
|
||||
finish()
|
||||
startActivity(newIntent)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun setupDrawer() {
|
||||
binding.mainDrawerButton.setOnClickListener{
|
||||
binding.drawerLayout.openDrawer(binding.drawer)
|
||||
}
|
||||
|
||||
header = AccountHeaderView(this).apply {
|
||||
attachToSliderView(binding.drawer)
|
||||
headerBackgroundScaleType = ImageView.ScaleType.CENTER_CROP
|
||||
currentHiddenInList = true
|
||||
onAccountHeaderListener = { _: View?, profile: IProfile, current: Boolean ->
|
||||
clickProfile(profile, current)
|
||||
}
|
||||
addProfile(ProfileSettingDrawerItem().apply {
|
||||
identifier = ADD_ACCOUNT_IDENTIFIER
|
||||
nameRes = R.string.add_account_name
|
||||
descriptionRes = R.string.add_account_description
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_add
|
||||
}, 0)
|
||||
dividerBelowHeader = false
|
||||
closeDrawerOnProfileListClick = true
|
||||
}
|
||||
|
||||
DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
|
||||
override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) {
|
||||
Glide.with(this@MainActivity)
|
||||
.load(uri)
|
||||
.placeholder(placeholder)
|
||||
.into(imageView)
|
||||
}
|
||||
|
||||
override fun cancel(imageView: ImageView) {
|
||||
Glide.with(this@MainActivity).clear(imageView)
|
||||
}
|
||||
|
||||
override fun placeholder(ctx: Context, tag: String?): Drawable {
|
||||
if (tag == DrawerImageLoader.Tags.PROFILE.name || tag == DrawerImageLoader.Tags.PROFILE_DRAWER_ITEM.name) {
|
||||
return ContextCompat.getDrawable(ctx, R.drawable.ic_default_user)!!
|
||||
}
|
||||
|
||||
return super.placeholder(ctx, tag)
|
||||
}
|
||||
})
|
||||
|
||||
fillDrawerAccountInfo(user!!.user_id)
|
||||
|
||||
//after setting with the values in the db, we make sure to update the database and apply
|
||||
//with the received one. This happens asynchronously.
|
||||
getUpdatedAccount()
|
||||
|
||||
binding.drawer.itemAdapter.add(
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.menu_account
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_person
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.menu_settings
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_settings
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.logout
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_close
|
||||
},
|
||||
)
|
||||
binding.drawer.onDrawerItemClickListener = { v, drawerItem, position ->
|
||||
when (position){
|
||||
1 -> launchActivity(ProfileActivity())
|
||||
2 -> launchActivity(SettingsActivity())
|
||||
3 -> logOut()
|
||||
4 -> launchActivity(ConversationsActivity())
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// Closes the drawer if it is open, when we press the back button
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
// Handle the back button event
|
||||
if(binding.drawerLayout.isDrawerOpen(GravityCompat.START)){
|
||||
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
||||
}
|
||||
else {
|
||||
this.isEnabled = false
|
||||
super.onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun logOut(){
|
||||
finish()
|
||||
|
||||
removeNotificationChannelsFromAccount(applicationContext, user)
|
||||
|
||||
db.runInTransaction {
|
||||
db.userDao().deleteActiveUsers()
|
||||
|
||||
val remainingUsers = db.userDao().getAll()
|
||||
if (remainingUsers.isEmpty()){
|
||||
//no more users, start first-time login flow
|
||||
launchActivity(LoginActivity(), firstTime = true)
|
||||
} else {
|
||||
val newActive = remainingUsers.first()
|
||||
db.userDao().activateUser(newActive.user_id, newActive.instance_uri)
|
||||
apiHolder.setToCurrentUser()
|
||||
//relaunch the app
|
||||
launchActivity(MainActivity(), firstTime = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUpdatedAccount() {
|
||||
if (hasInternet(applicationContext)) {
|
||||
|
||||
lifecycleScope.launchWhenCreated {
|
||||
try {
|
||||
val domain = user?.instance_uri.orEmpty()
|
||||
val accessToken = user?.accessToken.orEmpty()
|
||||
val refreshToken = user?.refreshToken
|
||||
val clientId = user?.clientId.orEmpty()
|
||||
val clientSecret = user?.clientSecret.orEmpty()
|
||||
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
|
||||
val account = api.verifyCredentials()
|
||||
addUser(db, account, domain, accessToken = accessToken, refreshToken = refreshToken, clientId = clientId, clientSecret = clientSecret)
|
||||
fillDrawerAccountInfo(account.id!!)
|
||||
} catch (exception: Exception) {
|
||||
Log.e("ACCOUNT UPDATE:", exception.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//called when switching profiles, or when clicking on current profile
|
||||
private fun clickProfile(profile: IProfile, current: Boolean): Boolean {
|
||||
if(current){
|
||||
launchActivity(ProfileActivity())
|
||||
return false
|
||||
}
|
||||
//Clicked on add new account
|
||||
if(profile.identifier == ADD_ACCOUNT_IDENTIFIER){
|
||||
launchActivity(LoginActivity())
|
||||
return false
|
||||
}
|
||||
|
||||
switchUser(profile.identifier.toString(), profile.tag as String)
|
||||
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
|
||||
finish()
|
||||
startActivity(intent)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun switchUser(userId: String, instance_uri: String) {
|
||||
db.userDao().deActivateActiveUsers()
|
||||
db.userDao().activateUser(userId, instance_uri)
|
||||
apiHolder.setToCurrentUser()
|
||||
}
|
||||
|
||||
private inline fun primaryDrawerItem(block: PrimaryDrawerItem.() -> Unit): PrimaryDrawerItem {
|
||||
return PrimaryDrawerItem()
|
||||
.apply {
|
||||
isSelectable = false
|
||||
isIconTinted = true
|
||||
}
|
||||
.apply(block)
|
||||
}
|
||||
|
||||
private fun fillDrawerAccountInfo(account: String) {
|
||||
val users = db.userDao().getAll().toMutableList()
|
||||
users.sortWith { l, r ->
|
||||
when {
|
||||
l.isActive && !r.isActive -> -1
|
||||
r.isActive && !l.isActive -> 1
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
val profiles: MutableList<IProfile> = users.map { user ->
|
||||
ProfileDrawerItem().apply {
|
||||
isSelected = user.isActive
|
||||
nameText = user.display_name
|
||||
iconUrl = user.avatar_static
|
||||
isNameShown = true
|
||||
identifier = user.user_id.toLong()
|
||||
descriptionText = user.fullHandle
|
||||
tag = user.instance_uri
|
||||
}
|
||||
}.toMutableList()
|
||||
|
||||
// reuse the already existing "add account" item
|
||||
header.profiles.orEmpty()
|
||||
.filter { it.identifier == ADD_ACCOUNT_IDENTIFIER }
|
||||
.take(1)
|
||||
.forEach { profiles.add(it) }
|
||||
|
||||
header.clear()
|
||||
header.profiles = profiles
|
||||
header.setActiveProfile(account.toLong())
|
||||
}
|
||||
|
||||
/**
|
||||
* Use reflection to make it a bit harder to swipe between tabs
|
||||
*/
|
||||
private fun ViewPager2.reduceDragSensitivity() {
|
||||
val recyclerViewField = ViewPager2::class.java.getDeclaredField("mRecyclerView")
|
||||
recyclerViewField.isAccessible = true
|
||||
val recyclerView = recyclerViewField.get(this) as RecyclerView
|
||||
|
||||
val touchSlopField = RecyclerView::class.java.getDeclaredField("mTouchSlop")
|
||||
touchSlopField.isAccessible = true
|
||||
val touchSlop = touchSlopField.get(recyclerView) as Int
|
||||
touchSlopField.set(recyclerView, touchSlop*NestedScrollableHost.touchSlopModifier)
|
||||
}
|
||||
|
||||
private fun setupTabs(tab_array: List<() -> Fragment>){
|
||||
binding.viewPager.reduceDragSensitivity()
|
||||
binding.viewPager.adapter = object : FragmentStateAdapter(this) {
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return tab_array[position]()
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return tab_array.size
|
||||
}
|
||||
}
|
||||
|
||||
binding.viewPager.registerOnPageChangeCallback(object : OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
val selected = when(position){
|
||||
0 -> R.id.page_1
|
||||
1 -> R.id.page_2
|
||||
2 -> R.id.page_3
|
||||
3 -> {
|
||||
setNotificationBadge(false)
|
||||
R.id.page_4
|
||||
}
|
||||
4 -> R.id.page_5
|
||||
else -> null
|
||||
}
|
||||
if (selected != null) {
|
||||
binding.tabs.selectedItemId = selected
|
||||
}
|
||||
super.onPageSelected(position)
|
||||
}
|
||||
})
|
||||
|
||||
fun MenuItem.itemPos(): Int? {
|
||||
return when(itemId){
|
||||
R.id.page_1 -> 0
|
||||
R.id.page_2 -> 1
|
||||
R.id.page_3 -> 2
|
||||
R.id.page_4 -> 3
|
||||
R.id.page_5 -> 4
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
binding.tabs.setOnItemSelectedListener {item ->
|
||||
item.itemPos()?.let {
|
||||
binding.viewPager.currentItem = it
|
||||
true
|
||||
} ?: false
|
||||
}
|
||||
|
||||
binding.tabs.setOnItemReselectedListener { item ->
|
||||
item.itemPos()?.let { position ->
|
||||
val page =
|
||||
//No clue why this works but it does. F to pay respects
|
||||
supportFragmentManager.findFragmentByTag("f$position")
|
||||
(page as? CachedFeedFragment<*>)?.onTabReClicked()
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch one notification to show a badge if there are new notifications
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
user?.let {
|
||||
val lastNotification = db.notificationDao().latestNotification(it.user_id, it.instance_uri)
|
||||
try {
|
||||
val notification: List<Notification>? = apiHolder.api?.notifications(
|
||||
min_id = lastNotification?.id,
|
||||
limit = "20"
|
||||
)
|
||||
val filtered = notification?.filter { notification ->
|
||||
lastNotification == null || (notification.created_at
|
||||
?: Instant.MIN) > (lastNotification.created_at ?: Instant.MIN)
|
||||
}
|
||||
val numberOfNewNotifications = if((filtered?.size ?: 20) >= 20) null else filtered?.size
|
||||
if(filtered?.isNotEmpty() == true ) setNotificationBadge(true, numberOfNewNotifications)
|
||||
} catch (exception: Exception) {
|
||||
return@repeatOnLifecycle
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setNotificationBadge(show: Boolean, count: Int? = null){
|
||||
if(show){
|
||||
val badge = binding.tabs.getOrCreateBadge(R.id.page_4)
|
||||
if (count != null) badge.number = count
|
||||
}
|
||||
else binding.tabs.removeBadge(R.id.page_4)
|
||||
}
|
||||
|
||||
fun BottomNavigationView.uncheckAllItems() {
|
||||
menu.setGroupCheckable(0, true, false)
|
||||
for (i in 0 until menu.size()) {
|
||||
menu.getItem(i).isChecked = false
|
||||
}
|
||||
menu.setGroupCheckable(0, true, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches the given activity and put it as the current one
|
||||
* @param firstTime to true means the task history will be reset (as if the app were
|
||||
* launched anew into this activity)
|
||||
*/
|
||||
private fun launchActivity(activity: AppCompatActivity, firstTime: Boolean = false) {
|
||||
val intent = Intent(this, activity::class.java)
|
||||
|
||||
if(firstTime){
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package org.pixeldroid.app.directMessages
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.os.Bundle
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.directMessages.ui.main.ConversationsFragment
|
||||
|
||||
class ConversationsActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_conversations)
|
||||
if (savedInstanceState == null) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.container, ConversationsFragment.newInstance())
|
||||
.commitNow()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package org.pixeldroid.app.directMessages.ui.main
|
||||
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import org.pixeldroid.app.R
|
||||
|
||||
class ConversationsFragment : Fragment() {
|
||||
|
||||
companion object {
|
||||
fun newInstance() = ConversationsFragment()
|
||||
}
|
||||
|
||||
private lateinit var viewModel: ConversationsViewModel
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
viewModel = ViewModelProvider(this).get(ConversationsViewModel::class.java)
|
||||
// TODO: Use the ViewModel to watch the variable containing DM data and show it
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
return inflater.inflate(R.layout.fragment_main, container, false)
|
||||
}
|
||||
|
||||
}
|
25
app/src/main/java/org/pixeldroid/app/directMessages/ui/main/ConversationsViewModel.kt
Normal file
25
app/src/main/java/org/pixeldroid/app/directMessages/ui/main/ConversationsViewModel.kt
Normal file
@ -0,0 +1,25 @@
|
||||
package org.pixeldroid.app.directMessages.ui.main
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import org.pixeldroid.app.utils.PixelDroidApplication
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
import javax.inject.Inject
|
||||
|
||||
class ConversationsViewModel(application: Application) : AndroidViewModel(application) {
|
||||
// TODO: Implement the ViewModel
|
||||
// API calls for DM, store results in some variable here
|
||||
@Inject
|
||||
lateinit var apiHolder: PixelfedAPIHolder
|
||||
|
||||
init {
|
||||
(application as PixelDroidApplication).getAppComponent().inject(this)
|
||||
}
|
||||
|
||||
suspend fun loadConversations() {
|
||||
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
val conversations = api.viewAllConversations()
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
package org.pixeldroid.app.directmessages
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.widget.EdgeEffect
|
||||
import androidx.dynamicanimation.animation.SpringAnimation
|
||||
import androidx.dynamicanimation.animation.SpringForce
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.pixeldroid.common.pxToDp
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
|
||||
/** The magnitude of translation distance while the list is over-scrolled. */
|
||||
private const val OVERSCROLL_TRANSLATION_MAGNITUDE = 0.4f
|
||||
|
||||
/** The magnitude of translation distance when the list reaches the edge on fling. */
|
||||
private const val FLING_TRANSLATION_MAGNITUDE = 0.5f
|
||||
|
||||
|
||||
/**
|
||||
* Replace edge effect by a bounce
|
||||
*/
|
||||
class BounceEdgeEffectFactory(val refreshCallback: () -> Unit, val context: Context) : RecyclerView.EdgeEffectFactory() {
|
||||
override fun createEdgeEffect(recyclerView: RecyclerView, direction: Int): EdgeEffect {
|
||||
|
||||
return object : EdgeEffect(recyclerView.context) {
|
||||
|
||||
// A reference to the [SpringAnimation] for this RecyclerView used to bring the item back after the over-scroll effect.
|
||||
var translationAnim: SpringAnimation? = null
|
||||
|
||||
override fun onPull(deltaDistance: Float) {
|
||||
super.onPull(deltaDistance)
|
||||
handlePull(deltaDistance)
|
||||
}
|
||||
|
||||
override fun onPull(deltaDistance: Float, displacement: Float) {
|
||||
super.onPull(deltaDistance, displacement)
|
||||
handlePull(deltaDistance)
|
||||
}
|
||||
|
||||
private fun handlePull(deltaDistance: Float) {
|
||||
// This is called on every touch event while the list is scrolled with a finger.
|
||||
|
||||
// Translate the recyclerView with the distance
|
||||
val sign = if (direction == DIRECTION_BOTTOM) -1 else 1
|
||||
val translationYDelta = sign * recyclerView.width * deltaDistance * OVERSCROLL_TRANSLATION_MAGNITUDE
|
||||
recyclerView.translationY += translationYDelta
|
||||
|
||||
translationAnim?.cancel()
|
||||
}
|
||||
|
||||
override fun onRelease() {
|
||||
super.onRelease()
|
||||
// The finger is lifted. Start the animation to bring translation back to the resting state.
|
||||
if (recyclerView.translationY != 0f) {
|
||||
if (direction == DIRECTION_BOTTOM && recyclerView.translationY.toInt().absoluteValue.pxToDp(context) > 50) {
|
||||
refreshCallback()
|
||||
}
|
||||
|
||||
translationAnim = createAnim()?.also { it.start() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAbsorb(velocity: Int) {
|
||||
super.onAbsorb(velocity)
|
||||
|
||||
// The list has reached the edge on fling.
|
||||
val sign = if (direction == DIRECTION_BOTTOM) -1 else 1
|
||||
|
||||
val translationVelocity = sign * velocity * FLING_TRANSLATION_MAGNITUDE
|
||||
translationAnim?.cancel()
|
||||
translationAnim = createAnim().setStartVelocity(translationVelocity)?.also { it.start() }
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas?): Boolean {
|
||||
// don't paint the usual edge effect
|
||||
return false
|
||||
}
|
||||
|
||||
override fun isFinished(): Boolean {
|
||||
// Without this, will skip future calls to onAbsorb()
|
||||
return translationAnim?.isRunning?.not() ?: true
|
||||
}
|
||||
|
||||
private fun createAnim() = SpringAnimation(recyclerView, SpringAnimation.TRANSLATION_Y)
|
||||
.setSpring(
|
||||
SpringForce()
|
||||
.setFinalPosition(0f)
|
||||
.setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY)
|
||||
.setStiffness(SpringForce.STIFFNESS_LOW)
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -1,109 +0,0 @@
|
||||
package org.pixeldroid.app.directmessages
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityConversationBinding
|
||||
import org.pixeldroid.app.directmessages.ConversationFragment.Companion.CONVERSATION_ID
|
||||
import org.pixeldroid.app.directmessages.ConversationFragment.Companion.PROFILE_ID
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.api.PixelfedAPI
|
||||
|
||||
class ConversationActivity : BaseActivity() {
|
||||
lateinit var binding: ActivityConversationBinding
|
||||
|
||||
private lateinit var conversationFragment: ConversationFragment
|
||||
|
||||
companion object {
|
||||
const val USERNAME = "ConversationActivityUsername"
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityConversationBinding.inflate(layoutInflater)
|
||||
|
||||
conversationFragment = ConversationFragment()
|
||||
|
||||
setContentView(binding.root)
|
||||
setSupportActionBar(binding.topBar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
val userName = intent?.getSerializableExtra(USERNAME) as? String?
|
||||
supportActionBar?.title = getString(R.string.dm_title, userName)
|
||||
|
||||
val conversationId = intent?.getSerializableExtra(CONVERSATION_ID) as String
|
||||
val pid = intent?.getSerializableExtra(PROFILE_ID) as String
|
||||
|
||||
activateCommenter(pid)
|
||||
|
||||
initConversationFragment(pid, conversationId, savedInstanceState)
|
||||
}
|
||||
|
||||
private fun activateCommenter(pid: String) {
|
||||
//Activate commenter
|
||||
binding.submitComment.setOnClickListener {
|
||||
val textIn = binding.editComment.text
|
||||
//Open text input
|
||||
if(textIn.isNullOrEmpty()) {
|
||||
Toast.makeText(
|
||||
binding.root.context,
|
||||
binding.root.context.getString(R.string.empty_comment),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} else {
|
||||
//Post the comment
|
||||
lifecycleScope.launchWhenCreated {
|
||||
apiHolder.api?.let { it1 -> sendMessage(it1, pid) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initConversationFragment(profileId: String, conversationId: String, savedInstanceState: Bundle?) {
|
||||
|
||||
val arguments = Bundle()
|
||||
arguments.putSerializable(CONVERSATION_ID, conversationId)
|
||||
arguments.putSerializable(PROFILE_ID, profileId)
|
||||
conversationFragment.arguments = arguments
|
||||
|
||||
//TODO finish work here! commentFragment needs the swiperefreshlayout.. how??
|
||||
//Maybe read https://archive.ph/G9VHW#selection-1324.2-1322.3 or further research
|
||||
if (savedInstanceState == null) {
|
||||
supportFragmentManager.commit {
|
||||
setReorderingAllowed(true)
|
||||
replace(R.id.conversationFragment, conversationFragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendMessage(
|
||||
api: PixelfedAPI,
|
||||
pid: String,
|
||||
) {
|
||||
val textIn = binding.editComment.text
|
||||
val nonNullText = textIn.toString()
|
||||
try {
|
||||
binding.submitComment.isEnabled = false
|
||||
binding.editComment.isEnabled = false
|
||||
api.sendDirectMessage(pid, nonNullText)
|
||||
|
||||
//Reload to add the comment to the comment section
|
||||
conversationFragment.adapter.refresh()
|
||||
|
||||
binding.editComment.isEnabled = true
|
||||
binding.editComment.text = null
|
||||
binding.submitComment.isEnabled = true
|
||||
} catch (exception: Exception) {
|
||||
Log.e("DM SEND ERROR", exception.toString())
|
||||
Toast.makeText(
|
||||
binding.root.context, binding.root.context.getString(R.string.comment_error),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,188 +0,0 @@
|
||||
package org.pixeldroid.app.directmessages
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.view.View.VISIBLE
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.bumptech.glide.Glide
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.DirectMessagesConversationItemBinding
|
||||
import org.pixeldroid.app.posts.AlbumActivity
|
||||
import org.pixeldroid.app.posts.AlbumViewModel
|
||||
import org.pixeldroid.app.posts.feeds.cachedFeeds.CachedFeedFragment
|
||||
import org.pixeldroid.app.posts.feeds.cachedFeeds.FeedContentRepository
|
||||
import org.pixeldroid.app.posts.feeds.cachedFeeds.FeedViewModel
|
||||
import org.pixeldroid.app.posts.feeds.cachedFeeds.ViewModelFactory
|
||||
import org.pixeldroid.app.utils.api.objects.Conversation
|
||||
import org.pixeldroid.app.utils.api.objects.Message
|
||||
import org.pixeldroid.app.utils.db.entities.DirectMessageDatabaseEntity
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
|
||||
|
||||
/**
|
||||
* Fragment for one Direct Messages conversation
|
||||
*/
|
||||
class ConversationFragment : CachedFeedFragment<DirectMessageDatabaseEntity>() {
|
||||
|
||||
companion object {
|
||||
const val CONVERSATION_ID = "ConversationFragmentConversationId"
|
||||
const val PROFILE_ID = "ConversationFragmentProfileId"
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
adapter = DirectMessagesListAdapter(apiHolder)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = super.createView(inflater, container, savedInstanceState, true)
|
||||
|
||||
val pid = arguments?.getSerializable(PROFILE_ID) as String
|
||||
val conversationId = arguments?.getSerializable(CONVERSATION_ID) as String
|
||||
|
||||
val dao = db.directMessagesConversationDao()
|
||||
val remoteMediator = ConversationRemoteMediator(apiHolder, db, pid, conversationId)
|
||||
// get the view model
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
viewModel = ViewModelProvider(
|
||||
requireActivity(),
|
||||
ViewModelFactory(
|
||||
db, dao, remoteMediator,
|
||||
FeedContentRepository(db, dao, remoteMediator, conversationId)
|
||||
)
|
||||
)["directMessagesConversation", FeedViewModel::class.java] as FeedViewModel<DirectMessageDatabaseEntity>
|
||||
|
||||
launch()
|
||||
initSearch()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
/**
|
||||
* View Holder for a [Conversation] RecyclerView list item.
|
||||
*/
|
||||
class DirectMessagesConversationViewHolder(val binding: DirectMessagesConversationItemBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
private var message: DirectMessageDatabaseEntity? = null
|
||||
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
message?.let {
|
||||
if (it.type == "photo") {
|
||||
val intent = Intent(itemView.context, AlbumActivity::class.java)
|
||||
|
||||
intent.putExtra(AlbumViewModel.ALBUM_IMAGES, ArrayList(it.carousel.orEmpty()))
|
||||
intent.putExtra(AlbumViewModel.ALBUM_INDEX, 0)
|
||||
|
||||
itemView.context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(
|
||||
message: DirectMessageDatabaseEntity?,
|
||||
api: PixelfedAPIHolder,
|
||||
lifecycleScope: LifecycleCoroutineScope,
|
||||
) {
|
||||
this.message = message
|
||||
|
||||
if(message?.isAuthor == true) {
|
||||
binding.messageIncoming.visibility = GONE
|
||||
binding.messageOutgoing.visibility = VISIBLE
|
||||
binding.textMessageOutgoing.text = message.text
|
||||
} else {
|
||||
binding.messageIncoming.visibility = VISIBLE
|
||||
binding.messageOutgoing.visibility = GONE
|
||||
binding.textMessageIncoming.text = message?.text ?: ""
|
||||
}
|
||||
|
||||
if (message?.type == "photo"){
|
||||
binding.imageMessageIncoming.visibility = VISIBLE
|
||||
binding.imageMessageOutgoing.visibility = VISIBLE
|
||||
binding.textMessageOutgoing.visibility = GONE
|
||||
binding.textMessageOutgoing.visibility = GONE
|
||||
Glide.with(if(message.isAuthor == true) binding.imageMessageOutgoing else binding.imageMessageIncoming)
|
||||
.load(message.media)
|
||||
.into(if(message.isAuthor == true) binding.imageMessageOutgoing else binding.imageMessageIncoming)
|
||||
} else {
|
||||
binding.imageMessageIncoming.visibility = GONE
|
||||
binding.imageMessageOutgoing.visibility = GONE
|
||||
binding.textMessageOutgoing.visibility = VISIBLE
|
||||
binding.textMessageIncoming.visibility = VISIBLE
|
||||
}
|
||||
|
||||
message?.created_at.let {
|
||||
// if (it == null) binding.messageTime.text = ""
|
||||
// else setTextViewFromISO8601(
|
||||
// it,
|
||||
// binding.messageTime,
|
||||
// false
|
||||
// )
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(parent: ViewGroup): DirectMessagesConversationViewHolder {
|
||||
val itemBinding = DirectMessagesConversationItemBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
)
|
||||
return DirectMessagesConversationViewHolder(itemBinding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
inner class DirectMessagesListAdapter(
|
||||
private val apiHolder: PixelfedAPIHolder,
|
||||
) : PagingDataAdapter<DirectMessageDatabaseEntity, RecyclerView.ViewHolder>(
|
||||
object : DiffUtil.ItemCallback<DirectMessageDatabaseEntity>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: DirectMessageDatabaseEntity,
|
||||
newItem: DirectMessageDatabaseEntity
|
||||
): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: DirectMessageDatabaseEntity,
|
||||
newItem: DirectMessageDatabaseEntity
|
||||
): Boolean =
|
||||
oldItem == newItem
|
||||
}
|
||||
) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return DirectMessagesConversationViewHolder.create(parent)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return R.layout.direct_messages_conversation_item
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val uiModel = getItem(position)
|
||||
uiModel?.let {
|
||||
(holder as DirectMessagesConversationViewHolder).bind(
|
||||
it,
|
||||
apiHolder,
|
||||
lifecycleScope
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
package org.pixeldroid.app.directmessages
|
||||
|
||||
import android.util.Log
|
||||
import androidx.paging.*
|
||||
import androidx.room.withTransaction
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
import org.pixeldroid.app.utils.db.entities.DirectMessageDatabaseEntity
|
||||
import java.lang.Exception
|
||||
import java.lang.NullPointerException
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* RemoteMediator for a Direct Messages conversation.
|
||||
*
|
||||
* A [RemoteMediator] defines a set of callbacks used to incrementally load data from a remote
|
||||
* source into a local source wrapped by a [PagingSource], e.g., loading data from network into
|
||||
* a local db cache.
|
||||
*/
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
class ConversationRemoteMediator @Inject constructor(
|
||||
private val apiHolder: PixelfedAPIHolder,
|
||||
private val db: AppDatabase,
|
||||
private val pid: String,
|
||||
private val conversationId: String
|
||||
) : RemoteMediator<Int, DirectMessageDatabaseEntity>() {
|
||||
|
||||
override suspend fun load(loadType: LoadType, state: PagingState<Int, DirectMessageDatabaseEntity>): MediatorResult {
|
||||
try {
|
||||
val user = db.userDao().getActiveUser()
|
||||
?: return MediatorResult.Error(NullPointerException("No active user exists"))
|
||||
|
||||
val nextPage = when (loadType) {
|
||||
LoadType.REFRESH -> null
|
||||
LoadType.PREPEND -> {
|
||||
// No prepend for the moment, might be nice to add later
|
||||
db.directMessagesConversationDao().lastMessageId(user.user_id, user.instance_uri, conversationId)
|
||||
?: return MediatorResult.Success(endOfPaginationReached = true)
|
||||
}
|
||||
LoadType.APPEND ->
|
||||
return MediatorResult.Success(endOfPaginationReached = true)
|
||||
}
|
||||
|
||||
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
val apiResponse =
|
||||
api.directMessagesConversation(
|
||||
pid = pid,
|
||||
max_id = nextPage,
|
||||
)
|
||||
//TODO prepend
|
||||
|
||||
val messages = apiResponse.messages.map {
|
||||
DirectMessageDatabaseEntity(
|
||||
it,
|
||||
conversationId,
|
||||
user
|
||||
)
|
||||
}
|
||||
|
||||
val endOfPaginationReached = messages.isEmpty()
|
||||
|
||||
db.withTransaction {
|
||||
// Clear table in the database
|
||||
if (loadType == LoadType.REFRESH) {
|
||||
db.directMessagesConversationDao().clearFeedContent(user.user_id, user.instance_uri, conversationId)
|
||||
}
|
||||
db.directMessagesConversationDao().insertAll(messages)
|
||||
}
|
||||
return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
|
||||
} catch (exception: Exception){
|
||||
return MediatorResult.Error(exception)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,119 +0,0 @@
|
||||
package org.pixeldroid.app.directmessages
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityFollowersBinding
|
||||
import org.pixeldroid.app.databinding.NewDmDialogBinding
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
|
||||
|
||||
class DirectMessagesActivity : BaseActivity() {
|
||||
lateinit var binding: ActivityFollowersBinding
|
||||
|
||||
private lateinit var conversationFragment: DirectMessagesFragment
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityFollowersBinding.inflate(layoutInflater)
|
||||
|
||||
conversationFragment = DirectMessagesFragment()
|
||||
|
||||
setContentView(binding.root)
|
||||
setSupportActionBar(binding.topBar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setTitle(R.string.direct_messages)
|
||||
|
||||
initConversationFragment(savedInstanceState)
|
||||
|
||||
addFab()
|
||||
}
|
||||
|
||||
private fun addFab() {
|
||||
// Create a Floating Action Button
|
||||
val fab = FloatingActionButton(this).apply {
|
||||
id = View.generateViewId()
|
||||
setImageResource(android.R.drawable.ic_dialog_email) // Example icon
|
||||
ViewCompat.setElevation(this, 8f) // Set elevation if needed
|
||||
}
|
||||
|
||||
// Set LayoutParams for the FAB
|
||||
val fabParams = CoordinatorLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
|
||||
gravity = android.view.Gravity.END or android.view.Gravity.BOTTOM
|
||||
marginEnd = 16
|
||||
bottomMargin = 16
|
||||
}
|
||||
|
||||
// Add the FAB to the CoordinatorLayout
|
||||
binding.coordinatorFollowers.addView(fab, fabParams)
|
||||
|
||||
fab.setOnClickListener {
|
||||
newDirectMessage()
|
||||
}
|
||||
}
|
||||
|
||||
private fun newDirectMessage() {
|
||||
val newDmDialogBinding =
|
||||
NewDmDialogBinding.inflate(LayoutInflater.from(this))
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.new_dm_conversation)
|
||||
.setMessage(R.string.dm_instruction)
|
||||
.setView(newDmDialogBinding.root)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val name = newDmDialogBinding.dmTarget.text.toString()
|
||||
val text = newDmDialogBinding.dmText.text.toString()
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
apiHolder.api?.let {
|
||||
val pid = it.lookupUser(name, remote = name.count { it == '@' } == 2)
|
||||
.firstOrNull {
|
||||
it.name == name
|
||||
}?.id ?: return@launch errorSending()
|
||||
it.sendDirectMessage(pid, text)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("DirectMessagesActivity", e.toString())
|
||||
errorSending()
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
//
|
||||
// conversation?.accounts?.firstOrNull()?.let {
|
||||
// val intent = Intent(itemView.context, ConversationActivity::class.java).apply {
|
||||
// putExtra(PROFILE_ID, it.id)
|
||||
// putExtra(CONVERSATION_ID, conversation?.id)
|
||||
// putExtra(USERNAME, it.getDisplayName())
|
||||
// }
|
||||
// startActivity(intent)
|
||||
// }
|
||||
}
|
||||
|
||||
private fun errorSending() {
|
||||
Snackbar.make(binding.root, R.string.new_dm_error, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
private fun initConversationFragment(savedInstanceState: Bundle?) {
|
||||
//TODO finish work here! commentFragment needs the swiperefreshlayout.. how??
|
||||
//Maybe read https://archive.ph/G9VHW#selection-1324.2-1322.3 or further research
|
||||
if (savedInstanceState == null) {
|
||||
supportFragmentManager.commit {
|
||||
setReorderingAllowed(true)
|
||||
replace(R.id.conversationFragment, conversationFragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,171 +0,0 @@
|
||||
package org.pixeldroid.app.directmessages
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.DirectMessagesListItemBinding
|
||||
import org.pixeldroid.app.directmessages.ConversationActivity.Companion.USERNAME
|
||||
import org.pixeldroid.app.directmessages.ConversationFragment.Companion.CONVERSATION_ID
|
||||
import org.pixeldroid.app.directmessages.ConversationFragment.Companion.PROFILE_ID
|
||||
import org.pixeldroid.app.posts.feeds.cachedFeeds.CachedFeedFragment
|
||||
import org.pixeldroid.app.posts.feeds.cachedFeeds.FeedViewModel
|
||||
import org.pixeldroid.app.posts.feeds.cachedFeeds.ViewModelFactory
|
||||
import org.pixeldroid.app.posts.parseHTMLText
|
||||
import org.pixeldroid.app.posts.setTextViewFromISO8601
|
||||
import org.pixeldroid.app.profile.ProfileActivity
|
||||
import org.pixeldroid.app.utils.api.objects.Account
|
||||
import org.pixeldroid.app.utils.api.objects.Conversation
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
|
||||
|
||||
/**
|
||||
* Fragment for the list of Direct Messages conversations.
|
||||
*/
|
||||
class DirectMessagesFragment : CachedFeedFragment<Conversation>() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
adapter = DirectMessagesListAdapter(apiHolder)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
// get the view model
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
viewModel = ViewModelProvider(
|
||||
requireActivity(),
|
||||
ViewModelFactory(db, db.directMessagesDao(), DirectMessagesRemoteMediator(apiHolder, db))
|
||||
)["directMessagesList", FeedViewModel::class.java] as FeedViewModel<Conversation>
|
||||
|
||||
launch()
|
||||
initSearch()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
/**
|
||||
* View Holder for a [Conversation] RecyclerView list item.
|
||||
*/
|
||||
class DirectMessagesListViewHolder(val binding: DirectMessagesListItemBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
private var conversation: Conversation? = null
|
||||
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
conversation?.accounts?.firstOrNull()?.let {
|
||||
val intent = Intent(itemView.context, ConversationActivity::class.java).apply {
|
||||
putExtra(PROFILE_ID, it.id)
|
||||
putExtra(CONVERSATION_ID, conversation?.id)
|
||||
putExtra(USERNAME, it.getDisplayName())
|
||||
}
|
||||
itemView.context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
binding.dmAvatar.setOnClickListener {
|
||||
conversation?.accounts?.firstOrNull()?.let {
|
||||
val intent = Intent(itemView.context, ProfileActivity::class.java).apply {
|
||||
putExtra(Account.ACCOUNT_TAG, it)
|
||||
}
|
||||
itemView.context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(
|
||||
conversation: Conversation?,
|
||||
api: PixelfedAPIHolder,
|
||||
lifecycleScope: LifecycleCoroutineScope,
|
||||
) {
|
||||
|
||||
this.conversation = conversation
|
||||
|
||||
val account = conversation?.accounts?.firstOrNull()
|
||||
|
||||
Glide.with(itemView).load(account?.anyAvatar()).circleCrop()
|
||||
.into(binding.dmAvatar)
|
||||
|
||||
binding.dmUsername.text = account?.getDisplayName()
|
||||
|
||||
binding.dmLastMessage.text = parseHTMLText(
|
||||
conversation?.last_status?.content ?: "",
|
||||
conversation?.last_status?.mentions,
|
||||
api,
|
||||
itemView.context,
|
||||
lifecycleScope
|
||||
)
|
||||
|
||||
conversation?.last_status?.created_at.let {
|
||||
if (it == null) binding.messageTime.text = ""
|
||||
else setTextViewFromISO8601(
|
||||
it,
|
||||
binding.messageTime,
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(parent: ViewGroup): DirectMessagesListViewHolder {
|
||||
val itemBinding = DirectMessagesListItemBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
)
|
||||
return DirectMessagesListViewHolder(itemBinding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
inner class DirectMessagesListAdapter(
|
||||
private val apiHolder: PixelfedAPIHolder,
|
||||
) : PagingDataAdapter<Conversation, RecyclerView.ViewHolder>(
|
||||
object : DiffUtil.ItemCallback<Conversation>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: Conversation,
|
||||
newItem: Conversation
|
||||
): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: Conversation,
|
||||
newItem: Conversation
|
||||
): Boolean =
|
||||
oldItem == newItem
|
||||
}
|
||||
) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return DirectMessagesListViewHolder.create(parent)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return R.layout.direct_messages_list_item
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val uiModel = getItem(position)
|
||||
uiModel?.let {
|
||||
(holder as DirectMessagesListViewHolder).bind(
|
||||
it,
|
||||
apiHolder,
|
||||
lifecycleScope
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,206 +0,0 @@
|
||||
package org.pixeldroid.app.login
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.BuildConfig
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityLoginBinding
|
||||
import org.pixeldroid.app.main.MainActivity
|
||||
import org.pixeldroid.app.settings.SettingsActivity
|
||||
import org.pixeldroid.app.settings.TutorialSettingsDialog.Companion.START_TUTORIAL
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.api.PixelfedAPI
|
||||
import org.pixeldroid.app.utils.openUrl
|
||||
|
||||
/**
|
||||
Overview of the flow of the login process: (boxes are requests done in parallel,
|
||||
since they do not depend on each other)
|
||||
|
||||
_________________________________
|
||||
|[PixelfedAPI.registerApplication]|
|
||||
|[PixelfedAPI.wellKnownNodeInfo] |
|
||||
̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅
|
||||
+----> [PixelfedAPI.nodeInfoSchema] (and then [PixelfedAPI.instance] if needed)
|
||||
+----> [promptOAuth]
|
||||
+----> [PixelfedAPI.obtainToken]
|
||||
+----> [PixelfedAPI.verifyCredentials]
|
||||
|
||||
*/
|
||||
|
||||
class LoginActivity : BaseActivity() {
|
||||
|
||||
companion object {
|
||||
private const val PACKAGE_ID = BuildConfig.APPLICATION_ID
|
||||
private const val PREFERENCE_NAME = "$PACKAGE_ID.loginPref"
|
||||
private const val SCOPE = "read write follow"
|
||||
}
|
||||
|
||||
private lateinit var oauthScheme: String
|
||||
private lateinit var preferences: SharedPreferences
|
||||
|
||||
private lateinit var binding: ActivityLoginBinding
|
||||
val model: LoginActivityViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityLoginBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
oauthScheme = getString(R.string.auth_scheme)
|
||||
preferences = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
binding.connectInstanceButton.setOnClickListener {
|
||||
hideKeyboard()
|
||||
model.registerAppToServer(binding.editText.text.toString())
|
||||
}
|
||||
binding.whatsAnInstanceTextView.setOnClickListener{ whatsAnInstance() }
|
||||
|
||||
// Enter button on keyboard should press the connect button
|
||||
binding.editText.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
binding.connectInstanceButton.performClick()
|
||||
return@setOnEditorActionListener true
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
model.promptOauth.collectLatest {
|
||||
it?.let {
|
||||
if (it.launch) promptOAuth(it.normalizedDomain, it.clientId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
model.finishedLogin.collectLatest {
|
||||
when (it) {
|
||||
LoginActivityViewModel.FinishedLogin.Finished -> {
|
||||
val intent = Intent(this@LoginActivity, MainActivity::class.java)
|
||||
intent.flags =
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
startActivity(intent)
|
||||
}
|
||||
LoginActivityViewModel.FinishedLogin.FinishedFirstTime -> MaterialAlertDialogBuilder(binding.root.context)
|
||||
.setMessage(R.string.first_time_question)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val intent = Intent(this@LoginActivity, SettingsActivity::class.java)
|
||||
intent.flags =
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
intent.putExtra(START_TUTORIAL, true)
|
||||
startActivity(intent)
|
||||
}
|
||||
.setNegativeButton(R.string.skip_tutorial) { _, _ -> model.finishLogin()}
|
||||
.show()
|
||||
LoginActivityViewModel.FinishedLogin.NotFinished -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
model.loadingState.collectLatest {
|
||||
when(it.loginState){
|
||||
LoginActivityViewModel.LoginState.LoadingState.Resting -> loadingAnimation(false)
|
||||
LoginActivityViewModel.LoginState.LoadingState.Busy -> loadingAnimation(true)
|
||||
LoginActivityViewModel.LoginState.LoadingState.Error -> failedRegistration(it.error!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
val url: Uri? = intent.data
|
||||
|
||||
//Check if the activity was started after the authentication
|
||||
if (url == null || !url.toString().startsWith("$oauthScheme://$PACKAGE_ID")) return
|
||||
|
||||
val code = url.getQueryParameter("code")
|
||||
model.authenticate(code)
|
||||
}
|
||||
|
||||
private fun whatsAnInstance() {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setView(layoutInflater.inflate(R.layout.whats_an_instance_explanation, null))
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
// Create the AlertDialog
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun hideKeyboard() {
|
||||
val view = currentFocus
|
||||
if (view != null) {
|
||||
(getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).hideSoftInputFromWindow(
|
||||
view.windowToken,
|
||||
InputMethodManager.HIDE_NOT_ALWAYS
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun promptOAuth(normalizedDomain: String, client_id: String) {
|
||||
val url = "$normalizedDomain/oauth/authorize?" +
|
||||
"client_id" + "=" + client_id + "&" +
|
||||
"redirect_uri" + "=" + "$oauthScheme://$PACKAGE_ID" + "&" +
|
||||
"response_type=code" + "&" +
|
||||
"scope=${SCOPE.replace(" ", "%20")}"
|
||||
|
||||
if (!openUrl(url)) model.oauthLaunchFailed()
|
||||
else model.oauthLaunched()
|
||||
}
|
||||
|
||||
private fun failedRegistration(@StringRes message: Int = R.string.registration_failed) {
|
||||
when (message) {
|
||||
R.string.instance_not_pixelfed_warning -> MaterialAlertDialogBuilder(this@LoginActivity).apply {
|
||||
setMessage(R.string.instance_not_pixelfed_warning)
|
||||
setPositiveButton(R.string.instance_not_pixelfed_continue) { _, _ ->
|
||||
model.dialogAckedContinueAnyways()
|
||||
}
|
||||
setNegativeButton(R.string.instance_not_pixelfed_cancel) { _, _ ->
|
||||
model.dialogNegativeButtonClicked()
|
||||
}
|
||||
}.show()
|
||||
|
||||
R.string.api_not_enabled_dialog -> MaterialAlertDialogBuilder(this@LoginActivity).apply {
|
||||
setMessage(R.string.api_not_enabled_dialog)
|
||||
setNegativeButton(android.R.string.ok) { _, _ ->
|
||||
model.dialogNegativeButtonClicked()
|
||||
}
|
||||
}.show()
|
||||
|
||||
else -> binding.editText.error = getString(message)
|
||||
}
|
||||
loadingAnimation(false)
|
||||
}
|
||||
|
||||
private fun loadingAnimation(on: Boolean){
|
||||
if(on) {
|
||||
binding.loginActivityInstanceInputLayout.visibility = View.GONE
|
||||
binding.progressLayout.visibility = View.VISIBLE
|
||||
}
|
||||
else {
|
||||
binding.loginActivityInstanceInputLayout.visibility = View.VISIBLE
|
||||
binding.progressLayout.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,293 +0,0 @@
|
||||
package org.pixeldroid.app.login
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.gson.Gson
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.BuildConfig
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.utils.api.PixelfedAPI
|
||||
import org.pixeldroid.app.utils.api.objects.Application
|
||||
import org.pixeldroid.app.utils.api.objects.Instance
|
||||
import org.pixeldroid.app.utils.api.objects.NodeInfo
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.db.addUser
|
||||
import org.pixeldroid.app.utils.db.storeInstance
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
import org.pixeldroid.app.utils.normalizeDomain
|
||||
import org.pixeldroid.app.utils.notificationsWorker.makeChannelGroupId
|
||||
import org.pixeldroid.app.utils.notificationsWorker.makeNotificationChannels
|
||||
import org.pixeldroid.app.utils.validDomain
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class LoginActivityViewModel @Inject constructor(
|
||||
private val apiHolder: PixelfedAPIHolder,
|
||||
private val db: AppDatabase,
|
||||
@ApplicationContext private val applicationContext: Context,
|
||||
) : ViewModel() {
|
||||
companion object {
|
||||
private const val PACKAGE_ID = BuildConfig.APPLICATION_ID
|
||||
private const val PREFERENCE_NAME = "$PACKAGE_ID.loginPref"
|
||||
private const val SCOPE = "read write follow"
|
||||
}
|
||||
private val oauthScheme = applicationContext.getString(R.string.auth_scheme)
|
||||
|
||||
private lateinit var pixelfedAPI: PixelfedAPI
|
||||
private val preferences: SharedPreferences = applicationContext.getSharedPreferences(
|
||||
PREFERENCE_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
private val _loadingState: MutableStateFlow<LoginState> = MutableStateFlow(LoginState(LoginState.LoadingState.Resting))
|
||||
val loadingState = _loadingState.asStateFlow()
|
||||
|
||||
enum class FinishedLogin {
|
||||
NotFinished, Finished, FinishedFirstTime
|
||||
}
|
||||
private val _finishedLogin = MutableStateFlow(FinishedLogin.NotFinished)
|
||||
val finishedLogin = _finishedLogin.asStateFlow()
|
||||
|
||||
private val _promptOauth: MutableStateFlow<PromptOAuth?> = MutableStateFlow(null)
|
||||
val promptOauth = _promptOauth.asStateFlow()
|
||||
|
||||
data class PromptOAuth(
|
||||
val launch: Boolean,
|
||||
val normalizedDomain: String,
|
||||
val clientId: String,
|
||||
)
|
||||
|
||||
data class LoginState(
|
||||
val loginState: LoadingState,
|
||||
@StringRes
|
||||
val error: Int? = null,
|
||||
) {
|
||||
init {
|
||||
if (loginState == LoadingState.Error && error == null) throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
enum class LoadingState {
|
||||
Resting, Busy, Error
|
||||
}
|
||||
}
|
||||
|
||||
fun registerAppToServer(rawDomain: String) {
|
||||
val normalizedDomain = normalizeDomain(rawDomain)
|
||||
|
||||
if(!validDomain(normalizedDomain)) return failedRegistration(R.string.invalid_domain)
|
||||
|
||||
_loadingState.value = LoginState(LoginState.LoadingState.Busy)
|
||||
|
||||
pixelfedAPI = PixelfedAPI.createFromUrl(normalizedDomain)
|
||||
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val credentialsDeferred: Deferred<Application?> = async {
|
||||
try {
|
||||
pixelfedAPI.registerApplication(
|
||||
applicationContext.getString(R.string.app_name),
|
||||
"$oauthScheme://$PACKAGE_ID", SCOPE, "https://pixeldroid.org"
|
||||
)
|
||||
} catch (exception: Exception) {
|
||||
return@async null
|
||||
}
|
||||
}
|
||||
|
||||
val nodeInfoJRD = pixelfedAPI.wellKnownNodeInfo()
|
||||
|
||||
val credentials = credentialsDeferred.await()
|
||||
|
||||
val clientId = credentials?.client_id ?: return@launch failedRegistration()
|
||||
preferences.edit()
|
||||
.putString("clientID", clientId)
|
||||
.putString("clientSecret", credentials.client_secret)
|
||||
.apply()
|
||||
|
||||
|
||||
// c.f. https://nodeinfo.diaspora.software/protocol.html for more info
|
||||
val nodeInfoSchemaUrl = nodeInfoJRD.links.firstOrNull {
|
||||
it.rel == "http://nodeinfo.diaspora.software/ns/schema/2.0"
|
||||
}?.href ?: return@launch failedRegistration(R.string.instance_error)
|
||||
|
||||
nodeInfoSchema(normalizedDomain, clientId, nodeInfoSchemaUrl)
|
||||
} catch (exception: Exception) {
|
||||
return@launch failedRegistration()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun nodeInfoSchema(
|
||||
normalizedDomain: String,
|
||||
clientId: String,
|
||||
nodeInfoSchemaUrl: String
|
||||
) = coroutineScope {
|
||||
|
||||
val nodeInfo: NodeInfo = try {
|
||||
pixelfedAPI.nodeInfoSchema(nodeInfoSchemaUrl)
|
||||
} catch (exception: Exception) {
|
||||
return@coroutineScope failedRegistration(R.string.instance_error)
|
||||
}
|
||||
val domain: String = try {
|
||||
if (nodeInfo.hasInstanceEndpointInfo()) {
|
||||
preferences.edit().putString("nodeInfo", Gson().toJson(nodeInfo)).remove("instance").apply()
|
||||
nodeInfo.metadata?.config?.site?.url
|
||||
} else {
|
||||
val instance: Instance = try {
|
||||
pixelfedAPI.instance()
|
||||
} catch (exception: Exception) {
|
||||
return@coroutineScope failedRegistration(R.string.instance_error)
|
||||
}
|
||||
preferences.edit().putString("instance", Gson().toJson(instance)).remove("nodeInfo").apply()
|
||||
instance.uri
|
||||
}
|
||||
} catch (e: IllegalArgumentException){ null }
|
||||
?: return@coroutineScope failedRegistration(R.string.instance_error)
|
||||
|
||||
preferences.edit()
|
||||
.putString("domain", normalizeDomain(domain))
|
||||
.apply()
|
||||
|
||||
if (!nodeInfo.software?.name.orEmpty().contains("pixelfed")) {
|
||||
_loadingState.value = LoginState(LoginState.LoadingState.Error, R.string.instance_not_pixelfed_warning)
|
||||
_promptOauth.value = PromptOAuth(false, normalizedDomain, clientId)
|
||||
} else if (nodeInfo.metadata?.config?.features?.mobile_apis != true) {
|
||||
_loadingState.value = LoginState(LoginState.LoadingState.Error, R.string.api_not_enabled_dialog)
|
||||
} else {
|
||||
_promptOauth.value = PromptOAuth(true, normalizedDomain, clientId)
|
||||
_loadingState.value = LoginState(LoginState.LoadingState.Busy)
|
||||
}
|
||||
}
|
||||
|
||||
fun authenticate(code: String?) {
|
||||
_loadingState.value = LoginState(LoginState.LoadingState.Busy)
|
||||
// Get previous values from preferences
|
||||
val domain = preferences.getString("domain", "") as String
|
||||
val clientId = preferences.getString("clientID", "") as String
|
||||
val clientSecret = preferences.getString("clientSecret", "") as String
|
||||
|
||||
if (code.isNullOrBlank() || domain.isBlank() || clientId.isBlank() || clientSecret.isBlank()) {
|
||||
return failedRegistration(R.string.auth_failed)
|
||||
}
|
||||
|
||||
//Successful authorization
|
||||
pixelfedAPI = PixelfedAPI.createFromUrl(domain)
|
||||
val gson = Gson()
|
||||
val nodeInfo: NodeInfo? = gson.fromJson(preferences.getString("nodeInfo", null), NodeInfo::class.java)
|
||||
val instance: Instance? = gson.fromJson(preferences.getString("instance", null), Instance::class.java)
|
||||
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val token = pixelfedAPI.obtainToken(
|
||||
clientId, clientSecret, "$oauthScheme://$PACKAGE_ID",
|
||||
SCOPE, code,
|
||||
"authorization_code"
|
||||
)
|
||||
if (token.access_token == null) {
|
||||
return@launch failedRegistration(R.string.token_error)
|
||||
}
|
||||
storeInstance(db, nodeInfo, instance)
|
||||
storeUser(
|
||||
token.access_token,
|
||||
token.refresh_token,
|
||||
clientId,
|
||||
clientSecret,
|
||||
domain
|
||||
)
|
||||
wipeSharedSettings()
|
||||
} catch (exception: Exception) {
|
||||
return@launch failedRegistration(R.string.token_error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun storeUser(accessToken: String, refreshToken: String?, clientId: String, clientSecret: String, instance: String) {
|
||||
try {
|
||||
val firstTime = db.userDao().getActiveUser() == null
|
||||
val user = pixelfedAPI.verifyCredentials("Bearer $accessToken")
|
||||
db.userDao().deActivateActiveUsers()
|
||||
addUser(
|
||||
db,
|
||||
user,
|
||||
instance,
|
||||
activeUser = true,
|
||||
accessToken = accessToken,
|
||||
refreshToken = refreshToken,
|
||||
clientId = clientId,
|
||||
clientSecret = clientSecret
|
||||
)
|
||||
apiHolder.setToCurrentUser()
|
||||
|
||||
fetchNotifications()
|
||||
|
||||
_finishedLogin.value = if(firstTime) FinishedLogin.FinishedFirstTime else FinishedLogin.Finished
|
||||
} catch (exception: Exception) {
|
||||
return failedRegistration(R.string.verify_credentials)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Fetch the latest notifications of this account, to avoid launching old notifications
|
||||
private suspend fun fetchNotifications() {
|
||||
val user = db.userDao().getActiveUser()!!
|
||||
try {
|
||||
val notifications = apiHolder.api!!.notifications()
|
||||
|
||||
notifications.forEach{it.user_id = user.user_id; it.instance_uri = user.instance_uri}
|
||||
|
||||
db.notificationDao().insertAll(notifications)
|
||||
} catch (exception: Exception) {
|
||||
return failedRegistration(R.string.login_notifications)
|
||||
}
|
||||
|
||||
makeNotificationChannels(
|
||||
applicationContext,
|
||||
user.fullHandle,
|
||||
makeChannelGroupId(user)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun wipeSharedSettings(){
|
||||
preferences.edit().clear().apply()
|
||||
}
|
||||
|
||||
private fun failedRegistration(@StringRes message: Int = R.string.registration_failed) {
|
||||
_loadingState.value = LoginState(LoginState.LoadingState.Error, message)
|
||||
when (message) {
|
||||
R.string.instance_not_pixelfed_warning, R.string.api_not_enabled_dialog -> return
|
||||
else -> wipeSharedSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun oauthLaunched() {
|
||||
_promptOauth.value = null
|
||||
}
|
||||
|
||||
fun oauthLaunchFailed() {
|
||||
_promptOauth.value = null
|
||||
_loadingState.value = LoginState(LoginState.LoadingState.Error, R.string.browser_launch_failed)
|
||||
}
|
||||
|
||||
fun dialogAckedContinueAnyways() {
|
||||
_promptOauth.value = _promptOauth.value?.copy(launch = true)
|
||||
_loadingState.value = LoginState(LoginState.LoadingState.Busy)
|
||||
}
|
||||
|
||||
fun dialogNegativeButtonClicked() {
|
||||
wipeSharedSettings()
|
||||
_loadingState.value = LoginState(LoginState.LoadingState.Resting)
|
||||
}
|
||||
|
||||
fun finishLogin() {
|
||||
_finishedLogin.value = FinishedLogin.Finished
|
||||
}
|
||||
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
package org.pixeldroid.app.main
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.AccountListItemBinding
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
|
||||
class AccountListAdapter(
|
||||
private val items: StateFlow<List<UserDatabaseEntity>>,
|
||||
lifecycleScope: LifecycleCoroutineScope,
|
||||
private val onClick: (UserDatabaseEntity?) -> Unit
|
||||
) : RecyclerView.Adapter<AccountListAdapter.ViewHolder>() {
|
||||
private val itemsList: MutableList<UserDatabaseEntity> = mutableListOf()
|
||||
|
||||
init {
|
||||
lifecycleScope.launch {
|
||||
items.collect {
|
||||
itemsList.clear()
|
||||
itemsList.addAll(it.filter { !it.isActive })
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = AccountListItemBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.binding.root.setOnClickListener{onClick(itemsList.getOrNull(position))}
|
||||
if (position == itemsList.size) {
|
||||
Glide.with(holder.itemView)
|
||||
.load(R.drawable.add)
|
||||
.into(holder.binding.imageView)
|
||||
holder.binding.accountName.setText(R.string.add_account_name)
|
||||
holder.binding.accountUsername.setText(R.string.add_account_description)
|
||||
return
|
||||
}
|
||||
|
||||
val user = itemsList[position]
|
||||
Glide.with(holder.itemView)
|
||||
.load(user.avatar_static)
|
||||
.placeholder(R.drawable.ic_default_user)
|
||||
.circleCrop()
|
||||
.into(holder.binding.imageView)
|
||||
holder.binding.accountName.text = user.display_name
|
||||
holder.binding.accountUsername.text = user.fullHandle
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = itemsList.size + 1
|
||||
|
||||
class ViewHolder(val binding: AccountListItemBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
}
|
@ -1,902 +0,0 @@
|
||||
package org.pixeldroid.app.main
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
|
||||
import com.bumptech.glide.Glide
|
||||
import com.getkeepsafe.taptargetview.TapTarget
|
||||
import com.getkeepsafe.taptargetview.TapTargetView
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.google.android.material.navigation.NavigationBarView
|
||||
import com.google.android.material.navigation.NavigationView
|
||||
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.ProfileDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.interfaces.IProfile
|
||||
import com.mikepenz.materialdrawer.model.interfaces.descriptionRes
|
||||
import com.mikepenz.materialdrawer.model.interfaces.descriptionText
|
||||
import com.mikepenz.materialdrawer.model.interfaces.iconRes
|
||||
import com.mikepenz.materialdrawer.model.interfaces.iconUrl
|
||||
import com.mikepenz.materialdrawer.model.interfaces.nameRes
|
||||
import com.mikepenz.materialdrawer.model.interfaces.nameText
|
||||
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
|
||||
import com.mikepenz.materialdrawer.util.DrawerImageLoader
|
||||
import com.mikepenz.materialdrawer.widget.AccountHeaderView
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.ligi.tracedroid.sending.sendTraceDroidStackTracesIfExist
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityMainBinding
|
||||
import org.pixeldroid.app.directmessages.DirectMessagesActivity
|
||||
import org.pixeldroid.app.directmessages.DirectMessagesFragment
|
||||
import org.pixeldroid.app.login.LoginActivity
|
||||
import org.pixeldroid.app.postCreation.camera.CameraFragment
|
||||
import org.pixeldroid.app.posts.NestedScrollableHost
|
||||
import org.pixeldroid.app.posts.feeds.cachedFeeds.CachedFeedFragment
|
||||
import org.pixeldroid.app.posts.feeds.cachedFeeds.notifications.NotificationsFragment
|
||||
import org.pixeldroid.app.posts.feeds.cachedFeeds.postFeeds.PostFeedFragment
|
||||
import org.pixeldroid.app.posts.feeds.uncachedFeeds.UncachedPostsFragment
|
||||
import org.pixeldroid.app.profile.ProfileActivity
|
||||
import org.pixeldroid.app.searchDiscover.SearchDiscoverFragment
|
||||
import org.pixeldroid.app.settings.SettingsActivity
|
||||
import org.pixeldroid.app.settings.TutorialSettingsDialog.Companion.START_TUTORIAL
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.Tab
|
||||
import org.pixeldroid.app.utils.api.objects.Notification
|
||||
import org.pixeldroid.app.utils.api.objects.Tag
|
||||
import org.pixeldroid.app.utils.db.entities.HomeStatusDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.entities.PublicFeedStatusDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.updateUserInfoDb
|
||||
import org.pixeldroid.app.utils.hasInternet
|
||||
import org.pixeldroid.app.utils.loadDbMenuTabs
|
||||
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.INSTANCE_NOTIFICATION_TAG
|
||||
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.SHOW_NOTIFICATION_TAG
|
||||
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.USER_NOTIFICATION_TAG
|
||||
import org.pixeldroid.app.utils.notificationsWorker.enablePullNotifications
|
||||
import org.pixeldroid.app.utils.notificationsWorker.removeNotificationChannelsFromAccount
|
||||
import org.pixeldroid.common.dpToPx
|
||||
import java.time.Instant
|
||||
|
||||
|
||||
class MainActivity : BaseActivity() {
|
||||
|
||||
private lateinit var tabStored: List<Tab>
|
||||
private lateinit var header: AccountHeaderView
|
||||
private var user: UserDatabaseEntity? = null
|
||||
|
||||
private val model: MainActivityViewModel by viewModels()
|
||||
|
||||
companion object {
|
||||
const val ADD_ACCOUNT_IDENTIFIER: Long = -13
|
||||
}
|
||||
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
installSplashScreen().setOnExitAnimationListener {
|
||||
it.remove()
|
||||
}
|
||||
|
||||
// Workaround for dynamic colors not applying due to splash screen?
|
||||
DynamicColors.applyToActivityIfAvailable(this)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
//get the currently active user
|
||||
user = db.userDao().getActiveUser()
|
||||
|
||||
if (notificationFromOtherUser()) return
|
||||
|
||||
//Check if we have logged in and gotten an access token
|
||||
if (user == null) {
|
||||
finish()
|
||||
launchActivity(LoginActivity(), firstTime = true)
|
||||
} else {
|
||||
sendTraceDroidStackTracesIfExist("contact@pixeldroid.org", this)
|
||||
|
||||
setupDrawer()
|
||||
|
||||
setupTabs()
|
||||
|
||||
val showNotification: Boolean = intent.getBooleanExtra(SHOW_NOTIFICATION_TAG, false)
|
||||
val showTutorial: Int = intent.getIntExtra(START_TUTORIAL, -1)
|
||||
|
||||
if(showNotification){
|
||||
binding.viewPager.currentItem = 3
|
||||
} else if(showTutorial >= 0) {
|
||||
showTutorial(showTutorial)
|
||||
}
|
||||
|
||||
if (ActivityCompat.checkSelfPermission(applicationContext,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
) enablePullNotifications(this)
|
||||
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showTutorial(showTutorial: Int) {
|
||||
when(showTutorial){
|
||||
0 -> tutorialOnTabs(Tab.HOME_FEED)
|
||||
1 -> tutorialOnTabs(Tab.CREATE_FEED)
|
||||
2 -> dmTutorial()
|
||||
else -> return
|
||||
}
|
||||
}
|
||||
|
||||
private fun tutorialOnTabs(tab: Tab) {
|
||||
val target = (binding.tabs as? NavigationBarView)?.let{ findTab(it, tab) } ?: return //TODO tablet landscape not supported
|
||||
|
||||
when(tab){
|
||||
Tab.HOME_FEED -> homeTutorial(target)
|
||||
Tab.SEARCH_DISCOVER_FEED -> homeTutorialSearch(target)
|
||||
Tab.PUBLIC_FEED -> homeTutorialPublic(target)
|
||||
Tab.NOTIFICATIONS_FEED -> homeTutorialNotifications(target)
|
||||
Tab.CREATE_FEED -> createTutorial(target)
|
||||
else -> return
|
||||
}
|
||||
}
|
||||
|
||||
private fun dmTutorial(){
|
||||
val target = (binding.tabs as? NavigationBarView)?.let{ findTab(it, Tab.DIRECT_MESSAGES) } ?: binding.mainDrawerButton ?: return //TODO tablet landscape not supported
|
||||
|
||||
if(target is ImageButton) {
|
||||
TapTargetView.showFor(
|
||||
this@MainActivity,
|
||||
TapTarget.forView(target, getString(R.string.dm_tutorial_drawer))
|
||||
.transparentTarget(true)
|
||||
.targetRadius(60), // Specify the target radius (in dp)
|
||||
object : TapTargetView.Listener() {
|
||||
// The listener can listen for regular clicks, long clicks or cancels
|
||||
override fun onTargetClick(view: TapTargetView?) {
|
||||
super.onTargetClick(view) // This call is optional
|
||||
// Perform action for the current target
|
||||
target.performClick()
|
||||
dmTutorial2(null)
|
||||
}
|
||||
})
|
||||
} else dmTutorial2(target)
|
||||
}
|
||||
private fun findViewWithText(root: ViewGroup, text: String?): View? {
|
||||
for (i in 0 until root.childCount) {
|
||||
val child = root.getChildAt(i)
|
||||
if (child is TextView) {
|
||||
if (child.text.toString().contains(text!!)) {
|
||||
return child
|
||||
}
|
||||
}
|
||||
if (child is ViewGroup) {
|
||||
val result = findViewWithText(child, text)
|
||||
if (result != null) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
private fun dmTutorial2(target: View?) {
|
||||
lifecycleScope.launch {
|
||||
var target = target ?: findViewWithText(binding.drawer as ViewGroup, getString(R.string.direct_messages))
|
||||
while (target == null) {
|
||||
target = findViewWithText(binding.drawer as ViewGroup, getString(R.string.direct_messages))
|
||||
delay(100)
|
||||
}
|
||||
TapTargetView.showFor(
|
||||
this@MainActivity,
|
||||
TapTarget.forView(target, getString(R.string.direct_messages),
|
||||
getString(R.string.dm_tutorial_text))
|
||||
.transparentTarget(true)
|
||||
.targetRadius(60), // Specify the target radius (in dp)
|
||||
object : TapTargetView.Listener() {
|
||||
// The listener can listen for regular clicks, long clicks or cancels
|
||||
override fun onTargetClick(view: TapTargetView?) {
|
||||
super.onTargetClick(view) // This call is optional
|
||||
// Perform action for the current target
|
||||
target.performClick()
|
||||
//tutorialOnTabs(Tab.NOTIFICATIONS_FEED)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun homeTutorialPublic(target: View) {
|
||||
TapTargetView.showFor(
|
||||
this@MainActivity,
|
||||
TapTarget.forView(target, getString(R.string.public_feed),
|
||||
getString(R.string.public_feed_tutorial_explanation))
|
||||
.transparentTarget(true)
|
||||
.targetRadius(60), // Specify the target radius (in dp)
|
||||
object : TapTargetView.Listener() {
|
||||
// The listener can listen for regular clicks, long clicks or cancels
|
||||
override fun onTargetClick(view: TapTargetView?) {
|
||||
super.onTargetClick(view) // This call is optional
|
||||
// Perform action for the current target
|
||||
target.performClick()
|
||||
tutorialOnTabs(Tab.NOTIFICATIONS_FEED)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun homeTutorialNotifications(target: View) {
|
||||
TapTargetView.showFor(
|
||||
this@MainActivity,
|
||||
TapTarget.forView(target,
|
||||
getString(R.string.notifications_tutorial_title),
|
||||
getString(R.string.notifications_tutorial_explanation))
|
||||
.transparentTarget(true)
|
||||
.targetRadius(60), // Specify the target radius (in dp)
|
||||
object : TapTargetView.Listener() {
|
||||
// The listener can listen for regular clicks, long clicks or cancels
|
||||
override fun onTargetClick(view: TapTargetView?) {
|
||||
super.onTargetClick(view) // This call is optional
|
||||
// Perform action for the current target
|
||||
target.performClick()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun homeTutorialSearch(target: View) {
|
||||
TapTargetView.showFor(
|
||||
this@MainActivity,
|
||||
TapTarget.forView(target,
|
||||
getString(R.string.discover_tutorial_title),
|
||||
getString(R.string.discover_tutorial_explanation))
|
||||
.transparentTarget(true)
|
||||
.targetRadius(60), // Specify the target radius (in dp)
|
||||
object : TapTargetView.Listener() {
|
||||
// The listener can listen for regular clicks, long clicks or cancels
|
||||
override fun onTargetClick(view: TapTargetView?) {
|
||||
super.onTargetClick(view) // This call is optional
|
||||
// Perform action for the current target
|
||||
target.performClick()
|
||||
tutorialOnTabs(Tab.PUBLIC_FEED)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun homeTutorial(target: View) {
|
||||
TapTargetView.showFor(
|
||||
this@MainActivity,
|
||||
TapTarget.forView(target,
|
||||
getString(R.string.home_feed_tutorial_title),
|
||||
getString(R.string.home_feed_tutorial_explanation))
|
||||
.transparentTarget(true)
|
||||
.targetRadius(60), // Specify the target radius (in dp)
|
||||
object : TapTargetView.Listener() {
|
||||
// The listener can listen for regular clicks, long clicks or cancels
|
||||
override fun onTargetClick(view: TapTargetView?) {
|
||||
super.onTargetClick(view) // This call is optional
|
||||
// Perform action for the current target
|
||||
target.performClick()
|
||||
tutorialOnTabs(Tab.SEARCH_DISCOVER_FEED)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun createTutorial(target: View) {
|
||||
TapTargetView.showFor(
|
||||
this@MainActivity,
|
||||
TapTarget.forView(target, getString(R.string.create_tutorial_title),
|
||||
getString(R.string.create_tutorial_explanation))
|
||||
.transparentTarget(true)
|
||||
.targetRadius(60), // Specify the target radius (in dp)
|
||||
object : TapTargetView.Listener() {
|
||||
// The listener can listen for regular clicks, long clicks or cancels
|
||||
override fun onTargetClick(view: TapTargetView?) {
|
||||
super.onTargetClick(view) // This call is optional
|
||||
// Perform action for the current target
|
||||
target.performClick()
|
||||
lifecycleScope.launch {
|
||||
var targetCamera = findViewById<View>(R.id.camera_capture_button)
|
||||
while (targetCamera == null) {
|
||||
targetCamera = findViewById(R.id.camera_capture_button)
|
||||
delay(100)
|
||||
}
|
||||
TapTargetView.showFor(
|
||||
this@MainActivity,
|
||||
TapTarget.forView(targetCamera,
|
||||
getString(R.string.create_tutorial_title_2),
|
||||
getString(R.string.create_tutorial_explanation_2))
|
||||
.transparentTarget(true)
|
||||
.targetRadius(60),
|
||||
object : TapTargetView.Listener() {
|
||||
// The listener can listen for regular clicks, long clicks or cancels
|
||||
override fun onTargetClick(view: TapTargetView?) {
|
||||
super.onTargetClick(view) // This call is optional
|
||||
targetCamera.performClick()
|
||||
}
|
||||
|
||||
override fun onTargetCancel(view: TapTargetView?) {
|
||||
super.onTargetCancel(view)
|
||||
intent.removeExtra(START_TUTORIAL)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun findTab(navBar: NavigationBarView, tab: Tab): View? {
|
||||
val index = tabStored.indexOf(tab)
|
||||
for (i in 0 until navBar.childCount) {
|
||||
val child = navBar.getChildAt(i)
|
||||
if (child is ViewGroup) {
|
||||
return child.getChildAt(index)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private val notificationPermissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) { isGranted: Boolean ->
|
||||
if (isGranted) enablePullNotifications(this)
|
||||
}
|
||||
|
||||
// Checks if the activity was launched from a notification from another account than the
|
||||
// current active one, and if so switches to that account
|
||||
private fun notificationFromOtherUser(): Boolean {
|
||||
val userOfNotification: String? = intent.extras?.getString(USER_NOTIFICATION_TAG)
|
||||
val instanceOfNotification: String? = intent.extras?.getString(INSTANCE_NOTIFICATION_TAG)
|
||||
if (userOfNotification != null && instanceOfNotification != null
|
||||
&& (userOfNotification != user?.user_id
|
||||
|| instanceOfNotification != user?.instance_uri)
|
||||
) {
|
||||
|
||||
switchUser(userOfNotification, instanceOfNotification)
|
||||
|
||||
val newIntent = Intent(this, MainActivity::class.java)
|
||||
newIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
|
||||
if (intent.getBooleanExtra(SHOW_NOTIFICATION_TAG, false)) {
|
||||
newIntent.putExtra(SHOW_NOTIFICATION_TAG, true)
|
||||
}
|
||||
|
||||
finish()
|
||||
startActivity(newIntent)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun setupDrawer() {
|
||||
binding.mainDrawerButton?.setOnClickListener {
|
||||
binding.drawer?.let { drawer -> binding.drawerLayout.openDrawer(drawer) }
|
||||
}
|
||||
|
||||
val navigationHeader = binding.navigation?.getHeaderView(0) as? AccountHeaderView
|
||||
val headerview = navigationHeader ?: AccountHeaderView(this)
|
||||
|
||||
navigationHeader?.onAccountHeaderSelectionViewClickListener = { _: View, _: IProfile ->
|
||||
// update the arrow image within the drawer
|
||||
navigationHeader!!.accountSwitcherArrow.clearAnimation()
|
||||
|
||||
if(binding.accountList?.isVisible == true) {
|
||||
navigationHeader.accountSwitcherArrow.animate().rotation(0f).start()
|
||||
} else {
|
||||
navigationHeader.accountSwitcherArrow.animate().rotation(180f).start()
|
||||
|
||||
fun onAccountClick(user: UserDatabaseEntity?){
|
||||
clickProfile(user?.user_id, user?.instance_uri, false)
|
||||
}
|
||||
val adapter = AccountListAdapter(model.users, lifecycleScope, ::onAccountClick)
|
||||
binding.accountList?.adapter = adapter
|
||||
|
||||
val location = IntArray(2)
|
||||
navigationHeader.getLocationOnScreen(location)
|
||||
|
||||
// Set the position of textView within constraintLayout2
|
||||
val textViewLayoutParams = binding.accountList?.layoutParams as? ConstraintLayout.LayoutParams
|
||||
textViewLayoutParams?.topMargin = location[1] + (navigationHeader as ConstraintLayout).height - 6.dpToPx(this)
|
||||
binding.accountList?.layoutParams = textViewLayoutParams
|
||||
}
|
||||
binding.accountList?.isVisible = !(binding.accountList?.isVisible ?: false)
|
||||
true
|
||||
}
|
||||
|
||||
header = headerview.apply {
|
||||
binding.drawer?.let { attachToSliderView(it) }
|
||||
headerBackgroundScaleType = ImageView.ScaleType.CENTER_CROP
|
||||
currentHiddenInList = true
|
||||
onAccountHeaderListener = { _: View?, profile: IProfile, current: Boolean ->
|
||||
val userId: String? = if (profile.identifier == ADD_ACCOUNT_IDENTIFIER) null else profile.identifier.toString()
|
||||
clickProfile(userId, profile.tag?.toString(), current)
|
||||
}
|
||||
addProfile(ProfileSettingDrawerItem().apply {
|
||||
identifier = ADD_ACCOUNT_IDENTIFIER
|
||||
nameRes = R.string.add_account_name
|
||||
descriptionRes = R.string.add_account_description
|
||||
iconRes = R.drawable.add
|
||||
}, 0)
|
||||
dividerBelowHeader = false
|
||||
closeDrawerOnProfileListClick = true
|
||||
}
|
||||
|
||||
DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
|
||||
override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) {
|
||||
Glide.with(this@MainActivity)
|
||||
.load(uri)
|
||||
.placeholder(placeholder)
|
||||
.circleCrop()
|
||||
.into(imageView)
|
||||
}
|
||||
|
||||
override fun cancel(imageView: ImageView) {
|
||||
Glide.with(this@MainActivity).clear(imageView)
|
||||
}
|
||||
|
||||
override fun placeholder(ctx: Context, tag: String?): Drawable {
|
||||
if (tag == DrawerImageLoader.Tags.PROFILE.name || tag == DrawerImageLoader.Tags.PROFILE_DRAWER_ITEM.name) {
|
||||
return ContextCompat.getDrawable(ctx, R.drawable.ic_default_user)!!
|
||||
}
|
||||
|
||||
return super.placeholder(ctx, tag)
|
||||
}
|
||||
})
|
||||
|
||||
fillDrawerAccountInfo(user!!.user_id)
|
||||
|
||||
//after setting with the values in the db, we make sure to update the database and apply
|
||||
//with the received one. This happens asynchronously.
|
||||
getUpdatedAccount()
|
||||
|
||||
binding.drawer?.itemAdapter?.add(
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.direct_messages
|
||||
iconRes = R.drawable.message
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.menu_account
|
||||
iconRes = R.drawable.person
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.menu_settings
|
||||
iconRes = R.drawable.settings
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.logout
|
||||
iconRes = R.drawable.logout
|
||||
},
|
||||
)
|
||||
|
||||
binding.drawer?.onDrawerItemClickListener = { v, drawerItem, position ->
|
||||
when (position) {
|
||||
1 -> launchActivity(DirectMessagesActivity())
|
||||
2 -> launchActivity(ProfileActivity())
|
||||
3 -> launchActivity(SettingsActivity())
|
||||
4 -> logOut()
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// Closes the drawer if it is open, when we press the back button
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
// Handle the back button event
|
||||
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
||||
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
||||
} else {
|
||||
this.isEnabled = false
|
||||
super.onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun logOut(){
|
||||
finish()
|
||||
|
||||
removeNotificationChannelsFromAccount(applicationContext, user)
|
||||
|
||||
db.runInTransaction {
|
||||
db.userDao().deleteActiveUsers()
|
||||
|
||||
val remainingUsers = db.userDao().getAll()
|
||||
if (remainingUsers.isEmpty()){
|
||||
// No more users, start first-time login flow
|
||||
launchActivity(LoginActivity(), firstTime = true)
|
||||
} else {
|
||||
val newActive = remainingUsers.first()
|
||||
db.userDao().activateUser(newActive.user_id, newActive.instance_uri)
|
||||
apiHolder.setToCurrentUser()
|
||||
// Relaunch the app
|
||||
launchActivity(MainActivity(), firstTime = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUpdatedAccount() {
|
||||
if (hasInternet(applicationContext)) {
|
||||
|
||||
lifecycleScope.launchWhenCreated {
|
||||
try {
|
||||
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
|
||||
val account = api.verifyCredentials()
|
||||
updateUserInfoDb(db, account)
|
||||
|
||||
val show = api.getSettings().common.media.always_show_cw
|
||||
launch {
|
||||
PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||
.edit().putBoolean("always_show_nsfw", show).commit()
|
||||
}
|
||||
|
||||
//No need to update drawer account info here, the ViewModel listens to db updates
|
||||
} catch (exception: Exception) {
|
||||
Log.e("ACCOUNT UPDATE:", exception.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//called when switching profiles, or when clicking on current profile
|
||||
@Suppress("SameReturnValue")
|
||||
private fun clickProfile(id: String?, instance: String?, current: Boolean): Boolean {
|
||||
if(current){
|
||||
launchActivity(ProfileActivity())
|
||||
return false
|
||||
}
|
||||
//Clicked on add new account
|
||||
if(id == null || instance == null){
|
||||
launchActivity(LoginActivity())
|
||||
return false
|
||||
}
|
||||
|
||||
switchUser(id, instance)
|
||||
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
|
||||
finish()
|
||||
startActivity(intent)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun switchUser(userId: String, instance_uri: String) {
|
||||
db.runInTransaction{
|
||||
db.userDao().deActivateActiveUsers()
|
||||
db.userDao().activateUser(userId, instance_uri)
|
||||
apiHolder.setToCurrentUser()
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun primaryDrawerItem(block: PrimaryDrawerItem.() -> Unit): PrimaryDrawerItem {
|
||||
return PrimaryDrawerItem()
|
||||
.apply {
|
||||
isSelectable = false
|
||||
isIconTinted = true
|
||||
}
|
||||
.apply(block)
|
||||
}
|
||||
|
||||
private fun fillDrawerAccountInfo(account: String) {
|
||||
lifecycleScope.launch {
|
||||
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
model.users.collect { list ->
|
||||
val users = list.toMutableList()
|
||||
users.sortWith { l, r ->
|
||||
when {
|
||||
l.isActive && !r.isActive -> -1
|
||||
r.isActive && !l.isActive -> 1
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
val profiles: MutableList<IProfile> = users.map { user ->
|
||||
ProfileDrawerItem().apply {
|
||||
isSelected = user.isActive
|
||||
nameText = user.display_name
|
||||
iconUrl = user.avatar_static
|
||||
isNameShown = true
|
||||
identifier = user.user_id.toLong()
|
||||
descriptionText = user.fullHandle
|
||||
tag = user.instance_uri
|
||||
}
|
||||
}.toMutableList()
|
||||
|
||||
// reuse the already existing "add account" item
|
||||
header.profiles.orEmpty()
|
||||
.filter { it.identifier == ADD_ACCOUNT_IDENTIFIER }
|
||||
.take(1)
|
||||
.forEach { profiles.add(it) }
|
||||
|
||||
header.clear()
|
||||
header.profiles = profiles
|
||||
header.setActiveProfile(account.toLong())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use reflection to make it a bit harder to swipe between tabs
|
||||
*/
|
||||
private fun ViewPager2.reduceDragSensitivity() {
|
||||
val recyclerViewField = ViewPager2::class.java.getDeclaredField("mRecyclerView")
|
||||
recyclerViewField.isAccessible = true
|
||||
val recyclerView = recyclerViewField.get(this) as RecyclerView
|
||||
|
||||
val touchSlopField = RecyclerView::class.java.getDeclaredField("mTouchSlop")
|
||||
touchSlopField.isAccessible = true
|
||||
val touchSlop = touchSlopField.get(recyclerView) as Int
|
||||
touchSlopField.set(recyclerView, touchSlop*NestedScrollableHost.touchSlopModifier)
|
||||
}
|
||||
|
||||
private fun NavigationView.unSelectAll() {
|
||||
for (i in 0 until menu.size()) {
|
||||
val menuItem = menu.getItem(i)
|
||||
menuItem.isChecked = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun MenuItem.itemPos(): Int? {
|
||||
return when(itemId){
|
||||
R.id.page_1 -> 0
|
||||
R.id.page_2 -> 1
|
||||
R.id.page_3 -> 2
|
||||
R.id.page_4 -> 3
|
||||
R.id.page_5 -> 4
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun reclick(item: MenuItem) {
|
||||
item.itemPos()?.let { position ->
|
||||
val page =
|
||||
//No clue why this works but it does. F to pay respects
|
||||
supportFragmentManager.findFragmentByTag("f$position")
|
||||
(page as? CachedFeedFragment<*>)?.onTabReClicked()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
private fun setupTabs() {
|
||||
val tabsCheckedDbEntry = db.tabsDao().getTabsChecked(user!!.user_id, user!!.instance_uri)
|
||||
val pageIds = listOf(R.id.page_1, R.id.page_2, R.id.page_3, R.id.page_4, R.id.page_5)
|
||||
|
||||
fun Tab.getFragment(): (() -> Fragment) {
|
||||
return when (this) {
|
||||
Tab.HOME_FEED -> { {
|
||||
PostFeedFragment<HomeStatusDatabaseEntity>()
|
||||
.apply {
|
||||
arguments = Bundle().apply { putBoolean("home", true) }
|
||||
}
|
||||
} }
|
||||
Tab.SEARCH_DISCOVER_FEED -> { { SearchDiscoverFragment() } }
|
||||
Tab.CREATE_FEED -> { { CameraFragment().apply {
|
||||
arguments = Bundle().apply { putInt(START_TUTORIAL, intent.getIntExtra(START_TUTORIAL, -1)) }
|
||||
} } }
|
||||
Tab.NOTIFICATIONS_FEED -> { { NotificationsFragment() } }
|
||||
Tab.PUBLIC_FEED -> { {
|
||||
PostFeedFragment<PublicFeedStatusDatabaseEntity>()
|
||||
.apply {
|
||||
arguments = Bundle().apply { putBoolean("home", false) }
|
||||
}
|
||||
} }
|
||||
Tab.DIRECT_MESSAGES -> { {
|
||||
DirectMessagesFragment()
|
||||
} }
|
||||
Tab.HASHTAG_FEED -> { {
|
||||
UncachedPostsFragment()
|
||||
.apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(Tag.HASHTAG_TAG, this@getFragment.filter)
|
||||
}
|
||||
}
|
||||
} }
|
||||
}
|
||||
}
|
||||
|
||||
val (tabs, hashtagIndices) = if (tabsCheckedDbEntry.isEmpty()) {
|
||||
// Default menu
|
||||
Pair(
|
||||
Tab.defaultTabs,
|
||||
Tab.defaultTabs.map { 0 }
|
||||
)
|
||||
} else {
|
||||
Pair(
|
||||
// Get current menu visibility and order from settings
|
||||
loadDbMenuTabs(tabsCheckedDbEntry).filter { it.second }.map { it.first },
|
||||
// Get all hashtag feed indices
|
||||
db.tabsDao().getTabsChecked(user!!.user_id, user!!.instance_uri).filter {
|
||||
it.checked
|
||||
}.map {
|
||||
if (Tab.fromName(it.tab) == Tab.HASHTAG_FEED) {
|
||||
it.index
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
tabStored = tabs
|
||||
|
||||
val bottomNavigationMenu: Menu? = (binding.tabs as? NavigationBarView)?.menu?.apply {
|
||||
clear()
|
||||
}
|
||||
?: binding.navigation?.menu?.apply {
|
||||
if(tabs.contains(Tab.DIRECT_MESSAGES)) removeGroup(R.id.dmNavigationGroup)
|
||||
}
|
||||
|
||||
val user = db.userDao().getActiveUser()!!
|
||||
|
||||
hashtagIndices.zip(tabs).zip(pageIds).forEach { (indexPageId, pageId) ->
|
||||
val index = indexPageId.first
|
||||
val tabId = indexPageId.second
|
||||
|
||||
with(bottomNavigationMenu?.add(R.id.tabsId, pageId, 1, tabId.toLanguageString(this@MainActivity, db, index, true))) {
|
||||
val tabIcon = tabId.getDrawable(this@MainActivity)
|
||||
if (tabIcon != null) {
|
||||
this?.icon = tabIcon
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val tabArray: List<() -> Fragment> = tabs.map { it.getFragment() }
|
||||
binding.viewPager.reduceDragSensitivity()
|
||||
binding.viewPager.adapter = object : FragmentStateAdapter(this) {
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return tabArray[position]()
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return tabArray.size
|
||||
}
|
||||
}
|
||||
|
||||
val notificationId = tabs.zip(pageIds).find {
|
||||
it.first == Tab.NOTIFICATIONS_FEED
|
||||
}?.second
|
||||
|
||||
fun doAtPageId(pageId: Int): Int {
|
||||
if (notificationId != null && pageId == notificationId) {
|
||||
setNotificationBadge(false)
|
||||
}
|
||||
return pageId
|
||||
}
|
||||
|
||||
binding.viewPager.registerOnPageChangeCallback(object : OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
val selected = when(position){
|
||||
0 -> doAtPageId(R.id.page_1)
|
||||
1 -> doAtPageId(R.id.page_2)
|
||||
2 -> doAtPageId(R.id.page_3)
|
||||
3 -> doAtPageId(R.id.page_4)
|
||||
4 -> doAtPageId(R.id.page_5)
|
||||
else -> null
|
||||
}
|
||||
if (selected != null) {
|
||||
// Disable and re-enable reselected listener so that it's not triggered by this
|
||||
(binding.tabs as? NavigationBarView)?.setOnItemReselectedListener(null)
|
||||
(binding.tabs as? NavigationBarView)?.selectedItemId = selected
|
||||
(binding.tabs as? NavigationBarView)?.setOnItemReselectedListener(::reclick)
|
||||
|
||||
binding.navigation?.unSelectAll()
|
||||
binding.navigation?.menu?.getItem(position)?.setChecked(true)
|
||||
}
|
||||
super.onPageSelected(position)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
fun MenuItem.buttonPos() {
|
||||
when(itemId){
|
||||
R.id.dms -> launchActivity(DirectMessagesActivity())
|
||||
R.id.my_profile -> launchActivity(ProfileActivity())
|
||||
R.id.settings -> launchActivity(SettingsActivity())
|
||||
R.id.log_out -> logOut()
|
||||
}
|
||||
}
|
||||
|
||||
(binding.tabs as? NavigationBarView)?.setOnItemSelectedListener { item ->
|
||||
item.itemPos()?.let {
|
||||
binding.viewPager.currentItem = it
|
||||
true
|
||||
} ?: false
|
||||
}
|
||||
|
||||
(binding.tabs as? NavigationBarView)?.setOnItemReselectedListener(::reclick)
|
||||
|
||||
binding.navigation?.setNavigationItemSelectedListener { item ->
|
||||
if (binding.navigation?.menu?.children?.find { it.itemId == item.itemId }?.isChecked == true) {
|
||||
reclick(item)
|
||||
} else {
|
||||
item.itemPos()?.let {
|
||||
binding.navigation?.unSelectAll()
|
||||
item.isChecked = true
|
||||
binding.viewPager.currentItem = it
|
||||
true
|
||||
} ?: item.buttonPos()
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
// Fetch one notification to show a badge if there are new notifications
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
user?.let {
|
||||
val lastNotification = db.notificationDao().latestNotification(it.user_id, it.instance_uri)
|
||||
try {
|
||||
val notification: List<Notification>? = apiHolder.api?.notifications(
|
||||
min_id = lastNotification?.id,
|
||||
limit = "20"
|
||||
)
|
||||
val filtered = notification?.filter { notification ->
|
||||
lastNotification == null || (notification.created_at
|
||||
?: Instant.MIN) > (lastNotification.created_at ?: Instant.MIN)
|
||||
}
|
||||
val numberOfNewNotifications = if((filtered?.size ?: 20) >= 20) null else filtered?.size
|
||||
if(filtered?.isNotEmpty() == true ) setNotificationBadge(true, numberOfNewNotifications)
|
||||
} catch (exception: Exception) {
|
||||
return@repeatOnLifecycle
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setNotificationBadge(show: Boolean, count: Int? = null) {
|
||||
//TODO add badge to NavigationView... not implemented yet: https://github.com/material-components/material-components-android/issues/2860
|
||||
if(show){
|
||||
val badge = (binding.tabs as? NavigationBarView)?.getOrCreateBadge(R.id.page_4)
|
||||
if (count != null) badge?.number = count
|
||||
}
|
||||
else (binding.tabs as? NavigationBarView)?.removeBadge(R.id.page_4)
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches the given activity and put it as the current one
|
||||
* @param firstTime to true means the task history will be reset (as if the app were
|
||||
* launched anew into this activity)
|
||||
*/
|
||||
private fun launchActivity(activity: AppCompatActivity, firstTime: Boolean = false) {
|
||||
val intent = Intent(this, activity::class.java)
|
||||
|
||||
if(firstTime){
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
package org.pixeldroid.app.main
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class MainActivityViewModel @Inject constructor(
|
||||
private val db: AppDatabase
|
||||
): ViewModel() {
|
||||
|
||||
// Mutable state flow that will be used internally in the ViewModel, empty list is given as initial value.
|
||||
private val _users = MutableStateFlow(emptyList<UserDatabaseEntity>())
|
||||
|
||||
// Immutable state flow exposed to UI
|
||||
val users = _users.asStateFlow()
|
||||
|
||||
|
||||
init {
|
||||
getUsers()
|
||||
}
|
||||
|
||||
private fun getUsers() {
|
||||
viewModelScope.launch {
|
||||
db.userDao().getAllFlow().flowOn(Dispatchers.IO)
|
||||
.collect { users: List<UserDatabaseEntity> ->
|
||||
_users.update { users }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,193 +1,53 @@
|
||||
package org.pixeldroid.app.postCreation
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import android.os.*
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import com.getkeepsafe.taptargetview.TapTarget
|
||||
import com.getkeepsafe.taptargetview.TapTargetView
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityPostCreationBinding
|
||||
import org.pixeldroid.app.settings.TutorialSettingsDialog.Companion.START_TUTORIAL
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
|
||||
const val TAG = "Post Creation Activity"
|
||||
|
||||
class PostCreationActivity : BaseActivity() {
|
||||
|
||||
companion object {
|
||||
internal const val POST_DESCRIPTION = "post_description"
|
||||
internal const val PICTURE_DESCRIPTIONS = "picture_descriptions"
|
||||
internal const val PICTURE_DESCRIPTION = "picture_description"
|
||||
internal const val POST_REDRAFT = "post_redraft"
|
||||
internal const val POST_NSFW = "post_nsfw"
|
||||
internal const val TEMP_FILES = "temp_files"
|
||||
|
||||
fun intentForUris(context: Context, uris: List<Uri>) =
|
||||
Intent(Intent.ACTION_SEND_MULTIPLE).apply {
|
||||
// Pass downloaded images to new post creation activity
|
||||
putParcelableArrayListExtra(
|
||||
Intent.EXTRA_STREAM, ArrayList(uris)
|
||||
)
|
||||
|
||||
uris.forEach {
|
||||
// Why are we using ClipData in addition to parcelableArrayListExtra here?
|
||||
// Because the FLAG_GRANT_READ_URI_PERMISSION needs to be applied to the URIs, and
|
||||
// for some reason it doesn't get applied to all of them when not using ClipData
|
||||
if (clipData == null) {
|
||||
clipData = ClipData("", emptyArray(), ClipData.Item(it))
|
||||
} else {
|
||||
clipData!!.addItem(ClipData.Item(it))
|
||||
}
|
||||
}
|
||||
|
||||
setClass(context, PostCreationActivity::class.java)
|
||||
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private var user: UserDatabaseEntity? = null
|
||||
private lateinit var instance: InstanceDatabaseEntity
|
||||
|
||||
private lateinit var binding: ActivityPostCreationBinding
|
||||
|
||||
private lateinit var navController: NavController
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
user = db.userDao().getActiveUser()
|
||||
|
||||
instance = user?.run {
|
||||
db.instanceDao().getAll().first { instanceDatabaseEntity ->
|
||||
instanceDatabaseEntity.uri.contains(instance_uri)
|
||||
}
|
||||
} ?: InstanceDatabaseEntity("", "")
|
||||
|
||||
binding = ActivityPostCreationBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
val navHostFragment =
|
||||
supportFragmentManager.findFragmentById(R.id.postCreationContainer) as NavHostFragment
|
||||
navController = navHostFragment.navController
|
||||
navController.setGraph(R.navigation.post_creation_graph)
|
||||
|
||||
if(intent.getBooleanExtra(START_TUTORIAL, false)) lifecycleScope.launch {
|
||||
var targetCamera = findViewById<View>(R.id.toggleStoryPost)
|
||||
while (targetCamera == null) {
|
||||
targetCamera = findViewById(R.id.toggleStoryPost)
|
||||
delay(100)
|
||||
}
|
||||
TapTargetView.showFor(
|
||||
this@PostCreationActivity, // `this` is an Activity
|
||||
TapTarget.forView(targetCamera,
|
||||
getString(R.string.story_tutorial_title),
|
||||
getString(R.string.story_tutorial_explanation))
|
||||
.transparentTarget(true)
|
||||
.targetRadius(60),
|
||||
object : TapTargetView.Listener() {
|
||||
// The listener can listen for regular clicks, long clicks or cancels
|
||||
override fun onTargetClick(view: TapTargetView?) {
|
||||
super.onTargetClick(view) // This call is optional
|
||||
findViewById<View>(R.id.buttonStory)?.performClick()
|
||||
TapTargetView.showFor(
|
||||
this@PostCreationActivity, // `this` is an Activity
|
||||
TapTarget.forView(findViewById(R.id.editPhotoButton),
|
||||
getString(R.string.edit_tutorial_title),
|
||||
getString(R.string.edit_tutorial_explanation))
|
||||
.transparentTarget(true)
|
||||
.targetRadius(60),
|
||||
object : TapTargetView.Listener() {
|
||||
// The listener can listen for regular clicks, long clicks or cancels
|
||||
override fun onTargetClick(view: TapTargetView?) {
|
||||
super.onTargetClick(view) // This call is optional
|
||||
findViewById<View>(R.id.editPhotoButton)?.performClick()
|
||||
TapTargetView.showFor(
|
||||
this@PostCreationActivity, // `this` is an Activity
|
||||
TapTarget.forView(findViewById(R.id.tv_caption),
|
||||
getString(R.string.media_description_tutorial_title),
|
||||
getString(R.string.media_description_tutorial_explanation))
|
||||
.transparentTarget(true)
|
||||
.targetRadius(60),
|
||||
object : TapTargetView.Listener() {
|
||||
// The listener can listen for regular clicks, long clicks or cancels
|
||||
override fun onTargetClick(view: TapTargetView?) {
|
||||
super.onTargetClick(view) // This call is optional
|
||||
findViewById<View>(R.id.tv_caption)?.performClick()
|
||||
lifecycleScope.launch {
|
||||
delay(1000)
|
||||
var tv_caption = findViewById<View>(R.id.tv_caption)
|
||||
while (tv_caption == null || tv_caption.visibility != View.VISIBLE) {
|
||||
tv_caption = findViewById(R.id.tv_caption)
|
||||
delay(100)
|
||||
}
|
||||
TapTargetView.showFor(
|
||||
this@PostCreationActivity, // `this` is an Activity
|
||||
TapTarget.forView(findViewById(R.id.post_creation_next_button),
|
||||
getString(
|
||||
R.string.picture_tutorial_title
|
||||
),
|
||||
getString(R.string.picture_tutorial_explanation))
|
||||
.transparentTarget(true)
|
||||
.targetRadius(60),
|
||||
object : TapTargetView.Listener() {
|
||||
// The listener can listen for regular clicks, long clicks or cancels
|
||||
override fun onTargetClick(view: TapTargetView?) {
|
||||
super.onTargetClick(view) // This call is optional
|
||||
findViewById<View>(R.id.post_creation_next_button)?.performClick()
|
||||
showAccountChooser()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun showAccountChooser() {
|
||||
lifecycleScope.launch {
|
||||
var toolbar = findViewById<View>(R.id.top_bar) as? MaterialToolbar
|
||||
while (toolbar == null) {
|
||||
toolbar = findViewById(R.id.top_bar) as? MaterialToolbar
|
||||
delay(100)
|
||||
}
|
||||
|
||||
TapTargetView.showFor(
|
||||
this@PostCreationActivity, // `this` is an Activity
|
||||
TapTarget.forToolbarMenuItem(
|
||||
toolbar,
|
||||
R.id.action_switch_accounts,
|
||||
getString(R.string.switch_accounts_tutorial_title),
|
||||
getString(R.string.switch_accounts_tutorial_explanation)
|
||||
)
|
||||
.transparentTarget(true)
|
||||
.targetRadius(60),
|
||||
object : TapTargetView.Listener() {
|
||||
// The listener can listen for regular clicks, long clicks or cancels
|
||||
override fun onTargetClick(view: TapTargetView?) {
|
||||
super.onTargetClick(view) // This call is optional
|
||||
showPostButton()
|
||||
}
|
||||
})
|
||||
}
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
return navController.navigateUp() || super.onSupportNavigateUp()
|
||||
}
|
||||
|
||||
private fun showPostButton() {
|
||||
TapTargetView.showFor(
|
||||
this@PostCreationActivity, // `this` is an Activity
|
||||
TapTarget.forView(findViewById(R.id.post_submission_send_button),
|
||||
getString(R.string.post_button_tutorial_title),
|
||||
getString(R.string.post_button_tutorial_explanation))
|
||||
.transparentTarget(true)
|
||||
.targetRadius(60),
|
||||
object : TapTargetView.Listener() {
|
||||
// The listener can listen for regular clicks, long clicks or cancels
|
||||
override fun onTargetClick(view: TapTargetView?) {
|
||||
super.onTargetClick(view) // This call is optional
|
||||
findViewById<View>(R.id.post_creation_next_button)?.performClick()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp() = navController.navigateUp() || super.onSupportNavigateUp()
|
||||
|
||||
}
|
@ -33,24 +33,29 @@ import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.FragmentPostCreationBinding
|
||||
import org.pixeldroid.app.postCreation.camera.CameraActivity
|
||||
import org.pixeldroid.app.postCreation.camera.CameraFragment
|
||||
import org.pixeldroid.app.postCreation.carousel.CarouselItem
|
||||
import org.pixeldroid.app.utils.BaseFragment
|
||||
import org.pixeldroid.app.utils.bindingLifecycleAware
|
||||
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import org.pixeldroid.app.utils.fileExtension
|
||||
import org.pixeldroid.app.utils.getMimeType
|
||||
import org.pixeldroid.media_editor.common.PICTURE_POSITION
|
||||
import org.pixeldroid.media_editor.common.PICTURE_URI
|
||||
import org.pixeldroid.media_editor.photoEdit.PhotoEditActivity
|
||||
import org.pixeldroid.media_editor.videoEdit.VideoEditActivity
|
||||
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity
|
||||
import java.io.File
|
||||
import java.io.OutputStream
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
|
||||
class PostCreationFragment : BaseFragment() {
|
||||
|
||||
private var user: UserDatabaseEntity? = null
|
||||
private var instance: InstanceDatabaseEntity = InstanceDatabaseEntity("", "")
|
||||
|
||||
private var binding: FragmentPostCreationBinding by bindingLifecycleAware()
|
||||
private val model: PostCreationViewModel by activityViewModels()
|
||||
private lateinit var model: PostCreationViewModel
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
@ -67,16 +72,30 @@ class PostCreationFragment : BaseFragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val user = db.userDao().getActiveUser()
|
||||
user = db.userDao().getActiveUser()
|
||||
|
||||
val instance = user?.run {
|
||||
db.instanceDao().getInstance(instance_uri)
|
||||
instance = user?.run {
|
||||
db.instanceDao().getAll().first { instanceDatabaseEntity ->
|
||||
instanceDatabaseEntity.uri.contains(instance_uri)
|
||||
}
|
||||
} ?: InstanceDatabaseEntity("", "")
|
||||
|
||||
model.getPhotoData().observe(viewLifecycleOwner) { newPhotoData: MutableList<PhotoData>? ->
|
||||
val _model: PostCreationViewModel by activityViewModels {
|
||||
PostCreationViewModelFactory(
|
||||
requireActivity().application,
|
||||
requireActivity().intent.clipData!!,
|
||||
instance,
|
||||
requireActivity().intent.getStringExtra(PostCreationActivity.PICTURE_DESCRIPTION),
|
||||
requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_NSFW, false),
|
||||
requireActivity().intent.getBooleanExtra(CameraFragment.CAMERA_ACTIVITY_STORY, false),
|
||||
)
|
||||
}
|
||||
model = _model
|
||||
|
||||
model.getPhotoData().observe(viewLifecycleOwner) { newPhotoData ->
|
||||
// update UI
|
||||
binding.carousel.addData(
|
||||
newPhotoData.orEmpty().map {
|
||||
newPhotoData.map {
|
||||
CarouselItem(
|
||||
it.imageUri, it.imageDescription, it.video,
|
||||
it.videoEncodeProgress, it.videoEncodeStabilizationFirstPass,
|
||||
@ -84,7 +103,7 @@ class PostCreationFragment : BaseFragment() {
|
||||
)
|
||||
}
|
||||
)
|
||||
binding.postCreationNextButton.isEnabled = newPhotoData?.isNotEmpty() ?: false
|
||||
binding.postCreationNextButton.isEnabled = newPhotoData.isNotEmpty()
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
@ -208,9 +227,10 @@ class PostCreationFragment : BaseFragment() {
|
||||
}
|
||||
|
||||
private val addPhotoResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
val uris = result.data?.extras?.getParcelableArrayList<Uri>(Intent.EXTRA_STREAM)
|
||||
if (result.resultCode == Activity.RESULT_OK && uris != null) {
|
||||
model.setImages(model.addPossibleImages(uris, emptyList()))
|
||||
if (result.resultCode == Activity.RESULT_OK && result.data?.clipData != null) {
|
||||
result.data?.clipData?.let {
|
||||
model.setImages(model.addPossibleImages(it))
|
||||
}
|
||||
} else if (result.resultCode != Activity.RESULT_CANCELED) {
|
||||
Toast.makeText(requireActivity(), R.string.add_images_error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
@ -308,7 +328,7 @@ class PostCreationFragment : BaseFragment() {
|
||||
ActivityResultContracts.StartActivityForResult()){
|
||||
result: ActivityResult? ->
|
||||
if (result?.resultCode == Activity.RESULT_OK && result.data != null) {
|
||||
val position: Int = result.data!!.getIntExtra(PICTURE_POSITION, 0)
|
||||
val position: Int = result.data!!.getIntExtra(PhotoEditActivity.PICTURE_POSITION, 0)
|
||||
model.modifyAt(position, result.data!!)
|
||||
?: Toast.makeText(requireActivity(), R.string.error_editing, Toast.LENGTH_SHORT).show()
|
||||
} else if(result?.resultCode != Activity.RESULT_CANCELED){
|
||||
@ -321,8 +341,8 @@ class PostCreationFragment : BaseFragment() {
|
||||
requireActivity(),
|
||||
if (model.getPhotoData().value!![position].video) VideoEditActivity::class.java else PhotoEditActivity::class.java
|
||||
)
|
||||
.putExtra(PICTURE_URI, model.getPhotoData().value!![position].imageUri)
|
||||
.putExtra(PICTURE_POSITION, position)
|
||||
.putExtra(PhotoEditActivity.PICTURE_URI, model.getPhotoData().value!![position].imageUri)
|
||||
.putExtra(PhotoEditActivity.PICTURE_POSITION, position)
|
||||
|
||||
editResultContract.launch(intent)
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.pixeldroid.app.postCreation
|
||||
|
||||
import android.content.Context
|
||||
import android.app.Application
|
||||
import android.content.ClipData
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
@ -11,16 +12,15 @@ import android.widget.Toast
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.jarsilio.android.scrambler.exceptions.UnsupportedFileFormatException
|
||||
import com.jarsilio.android.scrambler.stripMetadata
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
@ -31,27 +31,37 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import okhttp3.MultipartBody
|
||||
import org.pixeldroid.app.main.MainActivity
|
||||
import org.pixeldroid.app.MainActivity
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.postCreation.camera.CameraFragment
|
||||
import org.pixeldroid.app.utils.PixelDroidApplication
|
||||
import org.pixeldroid.app.utils.api.objects.Attachment
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
import org.pixeldroid.app.utils.fileExtension
|
||||
import org.pixeldroid.app.utils.getMimeType
|
||||
import org.pixeldroid.media_editor.common.PICTURE_URI
|
||||
import org.pixeldroid.media_editor.videoEdit.VideoEditActivity
|
||||
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity
|
||||
import retrofit2.HttpException
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.net.URI
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.collections.MutableList
|
||||
import kotlin.collections.MutableMap
|
||||
import kotlin.collections.arrayListOf
|
||||
import kotlin.collections.forEach
|
||||
import kotlin.collections.get
|
||||
import kotlin.collections.getOrNull
|
||||
import kotlin.collections.indexOfFirst
|
||||
import kotlin.collections.mutableListOf
|
||||
import kotlin.collections.mutableMapOf
|
||||
import kotlin.collections.plus
|
||||
import kotlin.collections.set
|
||||
import kotlin.collections.toMutableList
|
||||
import kotlin.math.ceil
|
||||
|
||||
const val TAG = "Post Creation ViewModel"
|
||||
|
||||
// Models the UI state for the PostCreationActivity
|
||||
data class PostCreationActivityUiState(
|
||||
@ -98,56 +108,35 @@ data class PhotoData(
|
||||
var videoEncodeError: Boolean = false,
|
||||
) : Parcelable
|
||||
|
||||
@HiltViewModel
|
||||
class PostCreationViewModel @Inject constructor(
|
||||
private val state: SavedStateHandle,
|
||||
@ApplicationContext private val applicationContext: Context,
|
||||
db: AppDatabase,
|
||||
): ViewModel() {
|
||||
class PostCreationViewModel(
|
||||
application: Application,
|
||||
clipdata: ClipData? = null,
|
||||
val instance: InstanceDatabaseEntity? = null,
|
||||
existingDescription: String? = null,
|
||||
existingNSFW: Boolean = false,
|
||||
storyCreation: Boolean = false,
|
||||
) : AndroidViewModel(application) {
|
||||
private var storyPhotoDataBackup: MutableList<PhotoData>? = null
|
||||
private val photoData: MutableLiveData<MutableList<PhotoData>> by lazy {
|
||||
//FIXME We should be able to access the Intent action somehow, to determine if there are
|
||||
// 1 or multiple Uris instead of relying on the ClassCastException
|
||||
|
||||
// This should not work like this (reading its source code, get() function should return null
|
||||
// if it's the wrong type but instead throws ClassCastException).
|
||||
// Lucky for us that it does though: we first try to get a single Uri (which we could be
|
||||
// getting from a share of a single picture to the app), when the cast to Uri fails
|
||||
// we try to get a list of Uris instead (casting ourselves from Parcelable as suggested
|
||||
// in get() documentation)
|
||||
val uris = try {
|
||||
val singleUri: Uri? = state[Intent.EXTRA_STREAM]
|
||||
listOfNotNull(singleUri)
|
||||
} catch (e: ClassCastException) {
|
||||
state.get<ArrayList<Parcelable>>(Intent.EXTRA_STREAM)?.map { it as Uri }
|
||||
MutableLiveData<MutableList<PhotoData>>().also {
|
||||
it.value = clipdata?.let { it1 -> addPossibleImages(it1, mutableListOf()) }
|
||||
}
|
||||
|
||||
MutableLiveData<MutableList<PhotoData>>(
|
||||
addPossibleImages(
|
||||
uris,
|
||||
state.get<ArrayList<String>>(PostCreationActivity.PICTURE_DESCRIPTIONS),
|
||||
previousList = mutableListOf()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private val instance = db.instanceDao().getActiveInstance()
|
||||
|
||||
@Inject
|
||||
lateinit var apiHolder: PixelfedAPIHolder
|
||||
|
||||
private val _uiState: MutableStateFlow<PostCreationActivityUiState>
|
||||
|
||||
init {
|
||||
(application as PixelDroidApplication).getAppComponent().inject(this)
|
||||
val sharedPreferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||
PreferenceManager.getDefaultSharedPreferences(application)
|
||||
val templateDescription = sharedPreferences.getString("prefill_description", "") ?: ""
|
||||
|
||||
val storyCreation: Boolean = state[CameraFragment.CAMERA_ACTIVITY_STORY] ?: false
|
||||
|
||||
_uiState = MutableStateFlow(PostCreationActivityUiState(
|
||||
newPostDescriptionText = state[PostCreationActivity.POST_DESCRIPTION] ?: templateDescription,
|
||||
nsfw = state[PostCreationActivity.POST_NSFW] ?: false,
|
||||
newPostDescriptionText = existingDescription ?: templateDescription,
|
||||
nsfw = existingNSFW,
|
||||
maxEntries = if(storyCreation) 1 else instance?.albumLimit,
|
||||
storyCreation = storyCreation
|
||||
))
|
||||
@ -172,41 +161,32 @@ class PostCreationViewModel @Inject constructor(
|
||||
fun getPhotoData(): LiveData<MutableList<PhotoData>> = photoData
|
||||
|
||||
/**
|
||||
* Will add as many images as possible to [photoData], from the [uris], and if
|
||||
* ([photoData].size + [uris].size) > uiState.value.maxEntries then it will only add as many images
|
||||
* Will add as many images as possible to [photoData], from the [clipData], and if
|
||||
* ([photoData].size + [clipData].itemCount) > uiState.value.maxEntries then it will only add as many images
|
||||
* as are legal (if any) and a dialog will be shown to the user alerting them of this fact.
|
||||
*/
|
||||
fun addPossibleImages(
|
||||
uris: List<Uri>?,
|
||||
descriptions: List<String>?,
|
||||
previousList: MutableList<PhotoData>? = photoData.value,
|
||||
): MutableList<PhotoData> {
|
||||
fun addPossibleImages(clipData: ClipData, previousList: MutableList<PhotoData>? = photoData.value): MutableList<PhotoData> {
|
||||
val dataToAdd: ArrayList<PhotoData> = arrayListOf()
|
||||
var count = uris?.size ?: 0
|
||||
uiState.value.maxEntries?.let { maxEntries ->
|
||||
if(count + (previousList?.size ?: 0) > maxEntries){
|
||||
var count = clipData.itemCount
|
||||
uiState.value.maxEntries?.let {
|
||||
if(count + (previousList?.size ?: 0) > it){
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(userMessage = applicationContext.getString(R.string.total_exceeds_album_limit).format(maxEntries))
|
||||
currentUiState.copy(userMessage = getApplication<PixelDroidApplication>().getString(R.string.total_exceeds_album_limit).format(it))
|
||||
}
|
||||
count = count.coerceAtMost(maxEntries - (previousList?.size ?: 0))
|
||||
count = count.coerceAtMost(it - (previousList?.size ?: 0))
|
||||
}
|
||||
if (count + (previousList?.size ?: 0) >= maxEntries) {
|
||||
if (count + (previousList?.size ?: 0) >= it) {
|
||||
// Disable buttons to add more images
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(addPhotoButtonEnabled = false)
|
||||
}
|
||||
}
|
||||
for ((i, uri) in uris.orEmpty().withIndex()) {
|
||||
val sizeAndVideoPair: Pair<Long, Boolean> =
|
||||
getSizeAndVideoValidate(uri, (previousList?.size ?: 0) + dataToAdd.size + 1)
|
||||
dataToAdd.add(
|
||||
PhotoData(
|
||||
imageUri = uri,
|
||||
size = sizeAndVideoPair.first,
|
||||
video = sizeAndVideoPair.second,
|
||||
imageDescription = descriptions?.getOrNull(i)
|
||||
)
|
||||
)
|
||||
for (i in 0 until count) {
|
||||
clipData.getItemAt(i).let {
|
||||
val sizeAndVideoPair: Pair<Long, Boolean> =
|
||||
getSizeAndVideoValidate(it.uri, (previousList?.size ?: 0) + dataToAdd.size + 1)
|
||||
dataToAdd.add(PhotoData(imageUri = it.uri, size = sizeAndVideoPair.first, video = sizeAndVideoPair.second, imageDescription = it.text?.toString()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -224,7 +204,7 @@ class PostCreationViewModel @Inject constructor(
|
||||
private fun getSizeAndVideoValidate(uri: Uri, editPosition: Int): Pair<Long, Boolean> {
|
||||
val size: Long =
|
||||
if (uri.scheme =="content") {
|
||||
applicationContext.contentResolver.query(uri, null, null, null, null)
|
||||
getApplication<PixelDroidApplication>().contentResolver.query(uri, null, null, null, null)
|
||||
?.use { cursor ->
|
||||
/* Get the column indexes of the data in the Cursor,
|
||||
* move to the first row in the Cursor, get the data,
|
||||
@ -241,12 +221,12 @@ class PostCreationViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
val sizeInkBytes = ceil(size.toDouble() / 1000).toLong()
|
||||
val type = uri.getMimeType(applicationContext.contentResolver)
|
||||
val type = uri.getMimeType(getApplication<PixelDroidApplication>().contentResolver)
|
||||
val isVideo = type.startsWith("video/")
|
||||
|
||||
if (isVideo && !instance!!.videoEnabled) {
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(userMessage = applicationContext.getString(R.string.video_not_supported))
|
||||
currentUiState.copy(userMessage = getApplication<PixelDroidApplication>().getString(R.string.video_not_supported))
|
||||
}
|
||||
}
|
||||
|
||||
@ -255,7 +235,7 @@ class PostCreationViewModel @Inject constructor(
|
||||
val maxSize = if (isVideo) instance.maxVideoSize else instance.maxPhotoSize
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
userMessage = applicationContext.getString(R.string.size_exceeds_instance_limit, editPosition, sizeInkBytes, maxSize)
|
||||
userMessage = getApplication<PixelDroidApplication>().getString(R.string.size_exceeds_instance_limit, editPosition, sizeInkBytes, maxSize)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -292,7 +272,7 @@ class PostCreationViewModel @Inject constructor(
|
||||
videoEncodeComplete = false
|
||||
|
||||
VideoEditActivity.startEncoding(imageUri, null, it,
|
||||
context = applicationContext,
|
||||
context = getApplication<PixelDroidApplication>(),
|
||||
registerNewFFmpegSession = ::registerNewFFmpegSession,
|
||||
trackTempFile = ::trackTempFile,
|
||||
videoEncodeProgress = ::videoEncodeProgress
|
||||
@ -300,7 +280,7 @@ class PostCreationViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
imageUri = data.getStringExtra(PICTURE_URI)!!.toUri()
|
||||
imageUri = data.getStringExtra(org.pixeldroid.media_editor.photoEdit.PhotoEditActivity.PICTURE_URI)!!.toUri()
|
||||
val (imageSize, imageVideo) = getSizeAndVideoValidate(imageUri, position)
|
||||
size = imageSize
|
||||
video = imageVideo
|
||||
@ -407,17 +387,17 @@ class PostCreationViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
for (data: PhotoData in getPhotoData().value ?: emptyList()) {
|
||||
val extension = data.imageUri.fileExtension(applicationContext.contentResolver)
|
||||
val extension = data.imageUri.fileExtension(getApplication<PixelDroidApplication>().contentResolver)
|
||||
|
||||
val strippedImage = File.createTempFile("temp_img", ".$extension", applicationContext.cacheDir)
|
||||
val strippedImage = File.createTempFile("temp_img", ".$extension", getApplication<PixelDroidApplication>().cacheDir)
|
||||
|
||||
val imageUri = data.imageUri
|
||||
|
||||
val (strippedOrNot, size) = try {
|
||||
val orientation = ExifInterface(applicationContext.contentResolver.openInputStream(imageUri)!!).getAttributeInt(
|
||||
val orientation = ExifInterface(getApplication<PixelDroidApplication>().contentResolver.openInputStream(imageUri)!!).getAttributeInt(
|
||||
ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
|
||||
|
||||
stripMetadata(imageUri, strippedImage, applicationContext.contentResolver)
|
||||
stripMetadata(imageUri, strippedImage, getApplication<PixelDroidApplication>().contentResolver)
|
||||
|
||||
// Restore EXIF orientation
|
||||
val exifInterface = ExifInterface(strippedImage)
|
||||
@ -429,11 +409,11 @@ class PostCreationViewModel @Inject constructor(
|
||||
strippedImage.delete()
|
||||
if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
|
||||
val imageInputStream = try {
|
||||
applicationContext.contentResolver.openInputStream(imageUri)!!
|
||||
getApplication<PixelDroidApplication>().contentResolver.openInputStream(imageUri)!!
|
||||
} catch (e: FileNotFoundException){
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
userMessage = applicationContext.getString(R.string.file_not_found,
|
||||
userMessage = getApplication<PixelDroidApplication>().getString(R.string.file_not_found,
|
||||
data.imageUri)
|
||||
)
|
||||
}
|
||||
@ -445,14 +425,14 @@ class PostCreationViewModel @Inject constructor(
|
||||
if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
userMessage = applicationContext.getString(R.string.file_not_found,
|
||||
userMessage = getApplication<PixelDroidApplication>().getString(R.string.file_not_found,
|
||||
data.imageUri)
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val type = data.imageUri.getMimeType(applicationContext.contentResolver)
|
||||
val type = data.imageUri.getMimeType(getApplication<PixelDroidApplication>().contentResolver)
|
||||
val imagePart = ProgressRequestBody(strippedOrNot, size, type)
|
||||
val requestBody = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
@ -502,7 +482,7 @@ class PostCreationViewModel @Inject constructor(
|
||||
currentUiState.copy(
|
||||
uploadErrorVisible = true,
|
||||
uploadErrorExplanationText = if(e is HttpException){
|
||||
applicationContext.getString(R.string.upload_error, e.code())
|
||||
getApplication<PixelDroidApplication>().getString(R.string.upload_error, e.code())
|
||||
} else "",
|
||||
uploadErrorExplanationVisible = e is HttpException,
|
||||
)
|
||||
@ -568,14 +548,14 @@ class PostCreationViewModel @Inject constructor(
|
||||
sensitive = nsfw
|
||||
)
|
||||
}
|
||||
Toast.makeText(applicationContext, applicationContext.getString(R.string.upload_post_success),
|
||||
Toast.makeText(getApplication(), getApplication<PixelDroidApplication>().getString(R.string.upload_post_success),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
val intent = Intent(applicationContext, MainActivity::class.java)
|
||||
val intent = Intent(getApplication(), MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
//TODO make the activity launch this instead (and surrounding toasts too)
|
||||
applicationContext.startActivity(intent)
|
||||
getApplication<PixelDroidApplication>().startActivity(intent)
|
||||
} catch (exception: IOException) {
|
||||
Toast.makeText(applicationContext, applicationContext.getString(R.string.upload_post_error),
|
||||
Toast.makeText(getApplication(), getApplication<PixelDroidApplication>().getString(R.string.upload_post_error),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
Log.e(TAG, exception.toString())
|
||||
_uiState.update { currentUiState ->
|
||||
@ -584,7 +564,7 @@ class PostCreationViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
} catch (exception: HttpException) {
|
||||
Toast.makeText(applicationContext, applicationContext.getString(R.string.upload_post_failed),
|
||||
Toast.makeText(getApplication(), getApplication<PixelDroidApplication>().getString(R.string.upload_post_failed),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
Log.e(TAG, exception.response().toString() + exception.message().toString())
|
||||
_uiState.update { currentUiState ->
|
||||
@ -629,7 +609,7 @@ class PostCreationViewModel @Inject constructor(
|
||||
|
||||
//Show message saying extraneous pictures were removed but can be restored
|
||||
newUiState = newUiState.copy(
|
||||
userMessage = applicationContext.getString(R.string.extraneous_pictures_stories)
|
||||
userMessage = getApplication<PixelDroidApplication>().getString(R.string.extraneous_pictures_stories)
|
||||
)
|
||||
}
|
||||
// Restore if backup not null and first value is unchanged
|
||||
@ -649,4 +629,10 @@ class PostCreationViewModel @Inject constructor(
|
||||
fun updateStoryReactions(checked: Boolean) { _uiState.update { it.copy(storyReactions = checked) } }
|
||||
|
||||
fun updateStoryReplies(checked: Boolean) { _uiState.update { it.copy(storyReplies = checked) } }
|
||||
}
|
||||
}
|
||||
|
||||
class PostCreationViewModelFactory(val application: Application, val clipdata: ClipData, val instance: InstanceDatabaseEntity, val existingDescription: String?, val existingNSFW: Boolean, val storyCreation: Boolean) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.getConstructor(Application::class.java, ClipData::class.java, InstanceDatabaseEntity::class.java, String::class.java, Boolean::class.java, Boolean::class.java).newInstance(application, clipdata, instance, existingDescription, existingNSFW, storyCreation)
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ class PostSubmissionFragment : BaseFragment() {
|
||||
private lateinit var instance: InstanceDatabaseEntity
|
||||
|
||||
private var binding: FragmentPostSubmissionBinding by bindingLifecycleAware()
|
||||
private val model: PostCreationViewModel by activityViewModels()
|
||||
private lateinit var model: PostCreationViewModel
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
@ -60,9 +60,23 @@ class PostSubmissionFragment : BaseFragment() {
|
||||
accounts = db.userDao().getAll()
|
||||
|
||||
instance = user?.run {
|
||||
db.instanceDao().getInstance(instance_uri)
|
||||
db.instanceDao().getAll().first { instanceDatabaseEntity ->
|
||||
instanceDatabaseEntity.uri.contains(instance_uri)
|
||||
}
|
||||
} ?: InstanceDatabaseEntity("", "")
|
||||
|
||||
val _model: PostCreationViewModel by activityViewModels {
|
||||
PostCreationViewModelFactory(
|
||||
requireActivity().application,
|
||||
requireActivity().intent.clipData!!,
|
||||
instance,
|
||||
requireActivity().intent.getStringExtra(PostCreationActivity.PICTURE_DESCRIPTION),
|
||||
requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_NSFW, false),
|
||||
requireActivity().intent.getBooleanExtra(CameraFragment.CAMERA_ACTIVITY_STORY, false)
|
||||
)
|
||||
}
|
||||
model = _model
|
||||
|
||||
// Display the values from the view model
|
||||
binding.nsfwSwitch.isChecked = model.uiState.value.nsfw
|
||||
binding.newPostDescriptionInputField.setText(model.uiState.value.newPostDescriptionText)
|
||||
|
@ -3,7 +3,7 @@ package org.pixeldroid.app.postCreation.camera
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import org.pixeldroid.app.main.MainActivity
|
||||
import org.pixeldroid.app.MainActivity
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityCameraBinding
|
||||
import org.pixeldroid.app.postCreation.camera.CameraFragment.Companion.CAMERA_ACTIVITY
|
||||
@ -32,7 +32,7 @@ class CameraActivity : BaseActivity() {
|
||||
// If this CameraActivity wasn't started from the shortcut,
|
||||
// tell the fragment it's in an activity (so that it sends back the result instead of
|
||||
// starting a new post creation process)
|
||||
if (intent.action != Intent.ACTION_VIEW) {
|
||||
if (intent.action != "android.intent.action.VIEW") {
|
||||
val arguments = Bundle()
|
||||
arguments.putBoolean(CAMERA_ACTIVITY, true)
|
||||
arguments.putBoolean(CAMERA_ACTIVITY_STORY, story)
|
||||
@ -47,7 +47,7 @@ class CameraActivity : BaseActivity() {
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
// If this CameraActivity wasn't started from the shortcut, behave as usual
|
||||
if (intent.action != Intent.ACTION_VIEW) return super.onOptionsItemSelected(item)
|
||||
if (intent.action != "android.intent.action.VIEW") return super.onOptionsItemSelected(item)
|
||||
|
||||
// Else, start a new MainActivity when "going back" on this activity
|
||||
when (item.itemId) {
|
||||
|
@ -2,6 +2,7 @@ package org.pixeldroid.app.postCreation.camera
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.ClipData
|
||||
import android.content.ContentUris
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
@ -37,8 +38,8 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.databinding.FragmentCameraBinding
|
||||
import org.pixeldroid.app.postCreation.PostCreationActivity
|
||||
import org.pixeldroid.app.settings.TutorialSettingsDialog.Companion.START_TUTORIAL
|
||||
import org.pixeldroid.app.utils.BaseFragment
|
||||
import org.pixeldroid.app.utils.bindingLifecycleAware
|
||||
import java.io.File
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
@ -59,7 +60,7 @@ class CameraFragment : BaseFragment() {
|
||||
|
||||
private val cameraLifecycleOwner = CameraLifecycleOwner()
|
||||
|
||||
private lateinit var binding: FragmentCameraBinding
|
||||
private var binding: FragmentCameraBinding by bindingLifecycleAware()
|
||||
|
||||
private var displayId: Int = -1
|
||||
private var lensFacing: Int = CameraSelector.LENS_FACING_BACK
|
||||
@ -69,7 +70,6 @@ class CameraFragment : BaseFragment() {
|
||||
|
||||
private var inActivity by Delegates.notNull<Boolean>()
|
||||
private var addToStory by Delegates.notNull<Boolean>()
|
||||
private var tutorial by Delegates.notNull<Int>()
|
||||
|
||||
private var filePermissionDialogLaunched: Boolean = false
|
||||
|
||||
@ -92,8 +92,6 @@ class CameraFragment : BaseFragment() {
|
||||
inActivity = arguments?.getBoolean(CAMERA_ACTIVITY) ?: false
|
||||
addToStory = arguments?.getBoolean(CAMERA_ACTIVITY_STORY) ?: false
|
||||
|
||||
tutorial = arguments?.getInt(START_TUTORIAL) ?: -1
|
||||
|
||||
binding = FragmentCameraBinding.inflate(layoutInflater)
|
||||
|
||||
return binding.root
|
||||
@ -329,7 +327,7 @@ class CameraFragment : BaseFragment() {
|
||||
}
|
||||
|
||||
private fun setupUploadImage() {
|
||||
val videoEnabled: Boolean = db.instanceDao().getActiveInstance().videoEnabled
|
||||
val videoEnabled: Boolean = db.instanceDao().getInstance(db.userDao().getActiveUser()!!.instance_uri).videoEnabled
|
||||
var mimeTypes: Array<String> = arrayOf("image/*")
|
||||
if(videoEnabled) mimeTypes += "video/*"
|
||||
|
||||
@ -452,7 +450,21 @@ class CameraFragment : BaseFragment() {
|
||||
|
||||
private fun startAlbumCreation(uris: ArrayList<String>) {
|
||||
|
||||
val intent = PostCreationActivity.intentForUris(requireContext(), uris.map { it.toUri() })
|
||||
val intent = Intent(requireActivity(), PostCreationActivity::class.java)
|
||||
.apply {
|
||||
uris.forEach{
|
||||
//Why are we using ClipData here? Because the FLAG_GRANT_READ_URI_PERMISSION
|
||||
//needs to be applied to the URIs, and this flag only applies to the
|
||||
//Intent's data and any URIs specified in its ClipData.
|
||||
if(clipData == null){
|
||||
clipData = ClipData("", emptyArray(), ClipData.Item(it.toUri()))
|
||||
} else {
|
||||
clipData!!.addItem(ClipData.Item(it.toUri()))
|
||||
}
|
||||
}
|
||||
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
|
||||
if(inActivity && !addToStory){
|
||||
requireActivity().setResult(Activity.RESULT_OK, intent)
|
||||
@ -461,9 +473,6 @@ class CameraFragment : BaseFragment() {
|
||||
if(addToStory){
|
||||
intent.putExtra(CAMERA_ACTIVITY_STORY, addToStory)
|
||||
}
|
||||
if(!inActivity && tutorial != -1){
|
||||
intent.putExtra(START_TUTORIAL, true)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
@ -3,99 +3,40 @@ package org.pixeldroid.app.posts
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.databinding.ActivityAlbumBinding
|
||||
import org.pixeldroid.app.utils.api.objects.Attachment
|
||||
|
||||
|
||||
class AlbumActivity : AppCompatActivity() {
|
||||
private val model: AlbumViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val binding = ActivityAlbumBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
binding.albumPager.adapter = AlbumViewPagerAdapter(
|
||||
model.uiState.value.mediaAttachments,
|
||||
setContentView(binding.root)
|
||||
val mediaAttachments = intent.getSerializableExtra("images") as ArrayList<Attachment>
|
||||
val index = intent.getIntExtra("index", 0)
|
||||
binding.albumPager.adapter = AlbumViewPagerAdapter(mediaAttachments,
|
||||
sensitive = false,
|
||||
opened = true,
|
||||
//In the activity, we assume we want to show everything
|
||||
alwaysShowNsfw = true,
|
||||
clickCallback = ::clickCallback
|
||||
alwaysShowNsfw = true
|
||||
)
|
||||
binding.albumPager.currentItem = index
|
||||
|
||||
binding.albumPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) { model.positionSelected(position) }
|
||||
})
|
||||
|
||||
if (model.uiState.value.mediaAttachments.size == 1) {
|
||||
if(mediaAttachments.size == 1){
|
||||
binding.albumPager.isUserInputEnabled = false
|
||||
} else if ((model.uiState.value.mediaAttachments.size) > 1) {
|
||||
}
|
||||
else if((mediaAttachments.size) > 1) {
|
||||
binding.postIndicator.setViewPager(binding.albumPager)
|
||||
binding.postIndicator.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.postIndicator.visibility = View.GONE
|
||||
}
|
||||
|
||||
// Not really necessary because the ViewPager saves its state in onSaveInstanceState, but
|
||||
// it's good to stay consistent in case something gets out of sync
|
||||
binding.albumPager.setCurrentItem(model.uiState.value.index, false)
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowTitleEnabled(false)
|
||||
supportActionBar?.setBackgroundDrawable(null)
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
model.uiState.collect { uiState ->
|
||||
binding.albumPager.currentItem = uiState.index
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
model.isActionBarHidden.collect { isActionBarHidden ->
|
||||
val windowInsetsController =
|
||||
WindowCompat.getInsetsController(this@AlbumActivity.window, binding.albumPager)
|
||||
if (isActionBarHidden) {
|
||||
// Configure the behavior of the hidden system bars
|
||||
windowInsetsController.systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
// Hide both the status bar and the navigation bar
|
||||
supportActionBar?.hide()
|
||||
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
|
||||
binding.postIndicator.visibility = View.GONE
|
||||
} else {
|
||||
// Configure the behavior of the hidden system bars
|
||||
windowInsetsController.systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
// Show both the status bar and the navigation bar
|
||||
supportActionBar?.show()
|
||||
windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
|
||||
if ((model.uiState.value.mediaAttachments.size) > 1) {
|
||||
binding.postIndicator.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback passed to the AlbumViewPagerAdapter to signal a single click on the image
|
||||
*/
|
||||
private fun clickCallback(){
|
||||
model.barHide()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
|
@ -1,46 +0,0 @@
|
||||
package org.pixeldroid.app.posts
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.pixeldroid.app.utils.api.objects.Attachment
|
||||
import javax.inject.Inject
|
||||
|
||||
data class AlbumUiState(
|
||||
val mediaAttachments: ArrayList<Attachment> = arrayListOf(),
|
||||
val index: Int = 0,
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
class AlbumViewModel @Inject constructor(state: SavedStateHandle) : ViewModel() {
|
||||
companion object {
|
||||
const val ALBUM_IMAGES = "AlbumViewImages"
|
||||
const val ALBUM_INDEX = "AlbumViewIndex"
|
||||
}
|
||||
|
||||
private val _uiState: MutableStateFlow<AlbumUiState>
|
||||
private val _isActionBarHidden: MutableStateFlow<Boolean>
|
||||
|
||||
init {
|
||||
_uiState = MutableStateFlow(AlbumUiState(
|
||||
mediaAttachments = state[ALBUM_IMAGES] ?: ArrayList(),
|
||||
index = state[ALBUM_INDEX] ?: 0
|
||||
))
|
||||
_isActionBarHidden = MutableStateFlow(false)
|
||||
}
|
||||
|
||||
val uiState: StateFlow<AlbumUiState> = _uiState.asStateFlow()
|
||||
val isActionBarHidden: StateFlow<Boolean> = _isActionBarHidden
|
||||
|
||||
fun barHide() {
|
||||
_isActionBarHidden.update { !it }
|
||||
}
|
||||
|
||||
fun positionSelected(position: Int) {
|
||||
_uiState.update { it.copy(index = position) }
|
||||
}
|
||||
}
|
@ -88,8 +88,8 @@ class NestedScrollableHost(context: Context, attrs: AttributeSet? = null) :
|
||||
}
|
||||
val intent = Intent(context, AlbumActivity::class.java)
|
||||
|
||||
intent.putExtra(AlbumViewModel.ALBUM_IMAGES, images)
|
||||
intent.putExtra(AlbumViewModel.ALBUM_INDEX, (child as ViewPager2).currentItem)
|
||||
intent.putExtra("images", images)
|
||||
intent.putExtra("index", (child as ViewPager2).currentItem)
|
||||
|
||||
context.startActivity(intent)
|
||||
|
||||
|
@ -6,7 +6,6 @@ import android.view.View
|
||||
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.pixeldroid.app.R
|
||||
@ -23,18 +22,15 @@ import org.pixeldroid.app.utils.api.objects.Status.Companion.VIEW_COMMENTS_TAG
|
||||
import org.pixeldroid.app.utils.displayDimensionsInPx
|
||||
|
||||
class PostActivity : BaseActivity() {
|
||||
lateinit var binding: ActivityPostBinding
|
||||
private lateinit var binding: ActivityPostBinding
|
||||
|
||||
private lateinit var commentFragment: CommentFragment
|
||||
private var commentFragment = CommentFragment()
|
||||
|
||||
private lateinit var status: Status
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityPostBinding.inflate(layoutInflater)
|
||||
|
||||
commentFragment = CommentFragment()
|
||||
|
||||
setContentView(binding.root)
|
||||
setSupportActionBar(binding.topBar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
@ -48,15 +44,14 @@ class PostActivity : BaseActivity() {
|
||||
supportActionBar?.title = getString(R.string.post_title).format(status.account?.getDisplayName())
|
||||
|
||||
val holder = StatusViewHolder(binding.postFragmentSingle)
|
||||
val (width, height) = displayDimensionsInPx()
|
||||
|
||||
holder.bind(
|
||||
status, apiHolder, db, lifecycleScope, Pair((width*.7).toInt(), height),
|
||||
status, apiHolder, db, lifecycleScope, displayDimensionsInPx(),
|
||||
requestPermissionDownloadPic, isActivity = true
|
||||
)
|
||||
|
||||
activateCommenter()
|
||||
initCommentsFragment(domain = user?.instance_uri.orEmpty(), savedInstanceState)
|
||||
initCommentsFragment(domain = user?.instance_uri.orEmpty())
|
||||
|
||||
if(viewComments || postComment){
|
||||
//Scroll already down as much as possible (since comments are not loaded yet)
|
||||
@ -101,26 +96,15 @@ class PostActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun initCommentsFragment(domain: String, savedInstanceState: Bundle?) {
|
||||
private fun initCommentsFragment(domain: String) {
|
||||
|
||||
val arguments = Bundle()
|
||||
arguments.putSerializable(COMMENT_STATUS_ID, status.id)
|
||||
arguments.putSerializable(COMMENT_DOMAIN, domain)
|
||||
commentFragment.arguments = arguments
|
||||
|
||||
//TODO finish work here! commentFragment needs the swiperefreshlayout.. how??
|
||||
//Maybe read https://archive.ph/G9VHW#selection-1324.2-1322.3 or further research
|
||||
if (savedInstanceState == null) {
|
||||
supportFragmentManager.commit {
|
||||
setReorderingAllowed(true)
|
||||
replace(R.id.commentFragment, commentFragment)
|
||||
}
|
||||
}
|
||||
|
||||
binding.swipeRefreshLayout.setOnRefreshListener {
|
||||
commentFragment.adapter.refresh()
|
||||
commentFragment.adapter.notifyDataSetChanged()
|
||||
}
|
||||
supportFragmentManager.beginTransaction()
|
||||
.add(R.id.commentFragment, commentFragment).commit()
|
||||
}
|
||||
|
||||
private suspend fun postComment(
|
||||
|
@ -2,22 +2,15 @@ package org.pixeldroid.app.posts
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityReportBinding
|
||||
import org.pixeldroid.app.posts.ReportActivityViewModel.UploadState
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.api.objects.Status
|
||||
|
||||
class ReportActivity : BaseActivity() {
|
||||
|
||||
private lateinit var binding: ActivityReportBinding
|
||||
private val model: ReportActivityViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@ -31,47 +24,42 @@ class ReportActivity : BaseActivity() {
|
||||
|
||||
binding.reportTargetTextview.text = getString(R.string.report_target).format(status?.account?.acct)
|
||||
|
||||
binding.textInputLayout.editText?.text = model.editable
|
||||
|
||||
binding.textInputLayout.editText?.doAfterTextChanged { model.textChanged(it) }
|
||||
binding.reportButton.setOnClickListener{
|
||||
binding.reportButton.visibility = View.INVISIBLE
|
||||
binding.reportProgressBar.visibility = View.VISIBLE
|
||||
|
||||
binding.reportButton.setOnClickListener {
|
||||
model.sendReport(status, binding.textInputLayout.editText?.text.toString())
|
||||
}
|
||||
binding.textInputLayout.editText?.isEnabled = false
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
model.reportSent.collect {
|
||||
reportStatus(it)
|
||||
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
|
||||
lifecycleScope.launchWhenCreated {
|
||||
try {
|
||||
api.report(
|
||||
status?.account?.id!!,
|
||||
listOf(status),
|
||||
binding.textInputLayout.editText?.text.toString()
|
||||
)
|
||||
|
||||
reportStatus(true)
|
||||
} catch (exception: Exception) {
|
||||
reportStatus(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun reportStatus(success: UploadState){
|
||||
when (success) {
|
||||
UploadState.initial -> {
|
||||
binding.reportProgressBar.visibility = View.GONE
|
||||
binding.reportButton.visibility = View.VISIBLE
|
||||
binding.reportSuccess.visibility = View.INVISIBLE
|
||||
}
|
||||
UploadState.success -> {
|
||||
binding.reportProgressBar.visibility = View.GONE
|
||||
binding.reportButton.visibility = View.INVISIBLE
|
||||
binding.reportSuccess.visibility = View.VISIBLE
|
||||
}
|
||||
UploadState.failed -> {
|
||||
binding.textInputLayout.error = getString(R.string.report_error)
|
||||
binding.reportButton.visibility = View.VISIBLE
|
||||
binding.textInputLayout.editText?.isEnabled = true
|
||||
binding.reportProgressBar.visibility = View.GONE
|
||||
binding.reportSuccess.visibility = View.GONE
|
||||
}
|
||||
UploadState.inProgress -> {
|
||||
binding.reportButton.visibility = View.INVISIBLE
|
||||
binding.reportProgressBar.visibility = View.VISIBLE
|
||||
binding.textInputLayout.editText?.isEnabled = false
|
||||
}
|
||||
private fun reportStatus(success: Boolean){
|
||||
if(success){
|
||||
binding.reportProgressBar.visibility = View.GONE
|
||||
binding.reportButton.visibility = View.INVISIBLE
|
||||
binding.reportSuccess.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.textInputLayout.error = getString(R.string.report_error)
|
||||
binding.reportButton.visibility = View.VISIBLE
|
||||
binding.textInputLayout.editText?.isEnabled = true
|
||||
binding.reportProgressBar.visibility = View.GONE
|
||||
binding.reportSuccess.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
package org.pixeldroid.app.posts
|
||||
|
||||
import android.text.Editable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.utils.api.objects.Status
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ReportActivityViewModel @Inject constructor(val apiHolder: PixelfedAPIHolder): ViewModel() {
|
||||
var editable: Editable? = null
|
||||
private set
|
||||
|
||||
private val _reportSent: MutableStateFlow<UploadState> = MutableStateFlow(UploadState.initial)
|
||||
val reportSent = _reportSent.asStateFlow()
|
||||
|
||||
enum class UploadState {
|
||||
initial, success, failed, inProgress
|
||||
}
|
||||
fun textChanged(it: Editable?) {
|
||||
editable = it
|
||||
}
|
||||
|
||||
fun sendReport(status: Status?, text: String) {
|
||||
_reportSent.value = UploadState.inProgress
|
||||
viewModelScope.launch {
|
||||
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
try {
|
||||
api.report(
|
||||
status?.account?.id!!,
|
||||
listOf(status),
|
||||
text
|
||||
)
|
||||
|
||||
_reportSent.value = UploadState.success
|
||||
} catch (exception: Exception) {
|
||||
_reportSent.value = UploadState.failed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,11 @@
|
||||
package org.pixeldroid.app.posts
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.ClipData
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager.PERMISSION_DENIED
|
||||
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.AnimatedVectorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
@ -17,7 +20,11 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@ -70,7 +77,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
||||
fun bind(
|
||||
status: Status?, pixelfedAPI: PixelfedAPIHolder, db: AppDatabase,
|
||||
lifecycleScope: LifecycleCoroutineScope, displayDimensionsInPx: Pair<Int, Int>,
|
||||
requestPermissionDownloadPic: ActivityResultLauncher<String>, isActivity: Boolean = false,
|
||||
requestPermissionDownloadPic: ActivityResultLauncher<String>, isActivity: Boolean = false
|
||||
) {
|
||||
|
||||
this.itemView.visibility = View.VISIBLE
|
||||
@ -152,7 +159,8 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
||||
if(!status?.media_attachments.isNullOrEmpty()) {
|
||||
setupPostPics(binding, request)
|
||||
} else {
|
||||
binding.postConstraint.visibility = View.GONE
|
||||
binding.postPager.visibility = View.GONE
|
||||
binding.postIndicator.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
@ -363,7 +371,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
||||
apiHolder: PixelfedAPIHolder,
|
||||
db: AppDatabase,
|
||||
lifecycleScope: LifecycleCoroutineScope,
|
||||
requestPermissionDownloadPic: ActivityResultLauncher<String>,
|
||||
requestPermissionDownloadPic: ActivityResultLauncher<String>
|
||||
){
|
||||
var bookmarked: Boolean? = null
|
||||
binding.statusMore.setOnClickListener {
|
||||
@ -441,7 +449,178 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
||||
|
||||
true
|
||||
}
|
||||
R.id.post_more_menu_redraft -> launchRedraftDialog(lifecycleScope, apiHolder, db)
|
||||
R.id.post_more_menu_redraft -> {
|
||||
MaterialAlertDialogBuilder(binding.root.context).apply {
|
||||
setMessage(R.string.redraft_dialog_launch)
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
// Create new post creation activity
|
||||
val intent =
|
||||
Intent(context, PostCreationActivity::class.java)
|
||||
|
||||
// Get descriptions and images from original post
|
||||
val postDescription = status?.content ?: ""
|
||||
val postAttachments =
|
||||
status?.media_attachments!! // Catch possible exception from !! (?)
|
||||
val postNSFW = status?.sensitive
|
||||
|
||||
val imageUriStrings = postAttachments.map { postAttachment ->
|
||||
postAttachment.url ?: ""
|
||||
}
|
||||
val imageNames = imageUriStrings.map { imageUriString ->
|
||||
Uri.parse(imageUriString).lastPathSegment.toString()
|
||||
}
|
||||
val downloadedFiles = imageNames.map { imageName ->
|
||||
File(context.cacheDir, imageName)
|
||||
}
|
||||
val imageUris = downloadedFiles.map { downloadedFile ->
|
||||
Uri.fromFile(downloadedFile)
|
||||
}
|
||||
val imageDescriptions = postAttachments.map { postAttachment ->
|
||||
fromHtml(postAttachment.description ?: "").toString()
|
||||
}
|
||||
val downloadRequests: List<Request> = imageUriStrings.map { imageUriString ->
|
||||
Request.Builder().url(imageUriString).build()
|
||||
}
|
||||
|
||||
val counter = AtomicInteger(0)
|
||||
|
||||
// Define callback function for after downloading the images
|
||||
fun continuation() {
|
||||
// Wait for all outstanding downloads to finish
|
||||
if (counter.incrementAndGet() == imageUris.size) {
|
||||
if (allFilesExist(imageNames)) {
|
||||
// Delete original post
|
||||
lifecycleScope.launch {
|
||||
deletePost(apiHolder.api ?: apiHolder.setToCurrentUser(), db)
|
||||
}
|
||||
|
||||
val counterInt = counter.get()
|
||||
Toast.makeText(
|
||||
binding.root.context,
|
||||
binding.root.context.resources.getQuantityString(
|
||||
R.plurals.items_load_success,
|
||||
counterInt,
|
||||
counterInt
|
||||
),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
// Pass downloaded images to new post creation activity
|
||||
intent.apply {
|
||||
imageUris.zip(imageDescriptions).map { (imageUri, imageDescription) ->
|
||||
ClipData.Item(imageDescription, null, imageUri)
|
||||
}.forEach { imageItem ->
|
||||
if (clipData == null) {
|
||||
clipData = ClipData(
|
||||
"",
|
||||
emptyArray(),
|
||||
imageItem
|
||||
)
|
||||
} else {
|
||||
clipData!!.addItem(imageItem)
|
||||
}
|
||||
}
|
||||
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
|
||||
// Pass post description of existing post to new post creation activity
|
||||
intent.putExtra(
|
||||
PostCreationActivity.PICTURE_DESCRIPTION,
|
||||
fromHtml(postDescription).toString()
|
||||
)
|
||||
if (imageNames.isNotEmpty()) {
|
||||
intent.putExtra(
|
||||
PostCreationActivity.TEMP_FILES,
|
||||
imageNames.toTypedArray()
|
||||
)
|
||||
}
|
||||
intent.putExtra(
|
||||
PostCreationActivity.POST_REDRAFT,
|
||||
true
|
||||
)
|
||||
intent.putExtra(
|
||||
PostCreationActivity.POST_NSFW,
|
||||
postNSFW
|
||||
)
|
||||
|
||||
// Launch post creation activity
|
||||
binding.root.context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!allFilesExist(imageNames)) {
|
||||
// Track download progress
|
||||
Toast.makeText(
|
||||
binding.root.context,
|
||||
binding.root.context.getString(R.string.image_download_downloading),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
// Iterate through all pictures of the original post
|
||||
downloadRequests.zip(downloadedFiles).forEach { (downloadRequest, downloadedFile) ->
|
||||
// Check whether image is in cache directory already (maybe rather do so using Glide in the future?)
|
||||
if (!downloadedFile.exists()) {
|
||||
OkHttpClient().newCall(downloadRequest)
|
||||
.enqueue(object : Callback {
|
||||
override fun onFailure(
|
||||
call: Call,
|
||||
e: IOException
|
||||
) {
|
||||
Looper.prepare()
|
||||
downloadedFile.delete()
|
||||
Toast.makeText(
|
||||
binding.root.context,
|
||||
binding.root.context.getString(R.string.redraft_post_failed_io_except),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun onResponse(
|
||||
call: Call,
|
||||
response: Response
|
||||
) {
|
||||
val sink: BufferedSink =
|
||||
downloadedFile.sink().buffer()
|
||||
sink.writeAll(response.body!!.source())
|
||||
sink.close()
|
||||
Looper.prepare()
|
||||
continuation()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
continuation()
|
||||
}
|
||||
}
|
||||
} catch (exception: HttpException) {
|
||||
Toast.makeText(
|
||||
binding.root.context,
|
||||
binding.root.context.getString(
|
||||
R.string.redraft_post_failed_error,
|
||||
exception.code()
|
||||
),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} catch (exception: IOException) {
|
||||
Toast.makeText(
|
||||
binding.root.context,
|
||||
binding.root.context.getString(R.string.redraft_post_failed_io_except),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
show()
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
@ -466,165 +645,6 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchRedraftDialog(
|
||||
lifecycleScope: LifecycleCoroutineScope,
|
||||
apiHolder: PixelfedAPIHolder,
|
||||
db: AppDatabase
|
||||
): Boolean {
|
||||
MaterialAlertDialogBuilder(binding.root.context).apply {
|
||||
setMessage(R.string.redraft_dialog_launch)
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
// Get descriptions and images from original post
|
||||
val postDescription = status?.content ?: ""
|
||||
val postAttachments =
|
||||
status?.media_attachments!! // TODO Catch possible exception from !! (?)
|
||||
val postNSFW = status?.sensitive
|
||||
|
||||
val imageUriStrings = postAttachments.map { postAttachment ->
|
||||
postAttachment.url ?: ""
|
||||
}
|
||||
val imageNames = imageUriStrings.map { imageUriString ->
|
||||
Uri.parse(imageUriString).lastPathSegment.toString()
|
||||
}
|
||||
val downloadedFiles = imageNames.map { imageName ->
|
||||
File(context.cacheDir, imageName)
|
||||
}
|
||||
val imageDescriptions = postAttachments.map { postAttachment ->
|
||||
fromHtml(
|
||||
postAttachment.description ?: ""
|
||||
).toString()
|
||||
}
|
||||
val downloadRequests: List<Request> =
|
||||
imageUriStrings.map { imageUriString ->
|
||||
Request.Builder().url(imageUriString).build()
|
||||
}
|
||||
|
||||
val imageUris = downloadedFiles.map { downloadedFile ->
|
||||
Uri.fromFile(downloadedFile)
|
||||
}
|
||||
|
||||
val counter = AtomicInteger(0)
|
||||
|
||||
// Define callback function for after downloading the images
|
||||
fun continuation() {
|
||||
// Wait for all outstanding downloads to finish
|
||||
if (counter.incrementAndGet() == imageUris.size) {
|
||||
if (allFilesExist(imageNames)) {
|
||||
// Delete original post
|
||||
lifecycleScope.launch {
|
||||
deletePost(
|
||||
apiHolder.api ?: apiHolder.setToCurrentUser(), db
|
||||
)
|
||||
}
|
||||
|
||||
val counterInt = counter.get()
|
||||
Toast.makeText(
|
||||
binding.root.context,
|
||||
binding.root.context.resources.getQuantityString(
|
||||
R.plurals.items_load_success, counterInt, counterInt
|
||||
),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
|
||||
// Create new post creation activity
|
||||
val intent = PostCreationActivity.intentForUris(context, imageUris).apply {
|
||||
putExtra(
|
||||
PostCreationActivity.PICTURE_DESCRIPTIONS,
|
||||
ArrayList(imageDescriptions)
|
||||
)
|
||||
// Pass post description of existing post to new post creation activity
|
||||
putExtra(
|
||||
PostCreationActivity.POST_DESCRIPTION,
|
||||
fromHtml(postDescription).toString()
|
||||
)
|
||||
if (imageNames.isNotEmpty()) {
|
||||
putExtra(
|
||||
PostCreationActivity.TEMP_FILES,
|
||||
imageNames.toTypedArray()
|
||||
)
|
||||
}
|
||||
putExtra(PostCreationActivity.POST_REDRAFT, true)
|
||||
putExtra(PostCreationActivity.POST_NSFW, postNSFW)
|
||||
}
|
||||
|
||||
// Launch post creation activity
|
||||
binding.root.context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!allFilesExist(imageNames)) {
|
||||
// Track download progress
|
||||
Toast.makeText(
|
||||
binding.root.context,
|
||||
binding.root.context.getString(R.string.image_download_downloading),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
// Iterate through all pictures of the original post
|
||||
downloadRequests.zip(downloadedFiles)
|
||||
.forEach { (downloadRequest, downloadedFile) ->
|
||||
// Check whether image is in cache directory already (maybe rather do so using Glide in the future?)
|
||||
if (!downloadedFile.exists()) {
|
||||
OkHttpClient().newCall(downloadRequest)
|
||||
.enqueue(object : Callback {
|
||||
override fun onFailure(
|
||||
call: Call,
|
||||
e: IOException,
|
||||
) {
|
||||
Looper.prepare()
|
||||
downloadedFile.delete()
|
||||
Toast.makeText(
|
||||
binding.root.context,
|
||||
binding.root.context.getString(
|
||||
R.string.redraft_post_failed_io_except
|
||||
),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun onResponse(
|
||||
call: Call,
|
||||
response: Response,
|
||||
) {
|
||||
val sink: BufferedSink =
|
||||
downloadedFile.sink().buffer()
|
||||
sink.writeAll(response.body!!.source())
|
||||
sink.close()
|
||||
Looper.prepare()
|
||||
continuation()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
continuation()
|
||||
}
|
||||
}
|
||||
} catch (exception: HttpException) {
|
||||
Toast.makeText(
|
||||
binding.root.context, binding.root.context.getString(
|
||||
R.string.redraft_post_failed_error, exception.code()
|
||||
), Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} catch (exception: IOException) {
|
||||
Toast.makeText(
|
||||
binding.root.context,
|
||||
binding.root.context.getString(R.string.redraft_post_failed_io_except),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
show()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun activateLiker(
|
||||
apiHolder: PixelfedAPIHolder,
|
||||
isLiked: Boolean,
|
||||
@ -800,15 +820,17 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
||||
class AlbumViewPagerAdapter(
|
||||
private val media_attachments: List<Attachment>, private var sensitive: Boolean?,
|
||||
private val opened: Boolean, private val alwaysShowNsfw: Boolean,
|
||||
private val clickCallback: (() -> Unit)? = null
|
||||
) : RecyclerView.Adapter<AlbumViewPagerAdapter.ViewHolder>() {
|
||||
) :
|
||||
RecyclerView.Adapter<AlbumViewPagerAdapter.ViewHolder>() {
|
||||
|
||||
private var isActionBarHidden: Boolean = false
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
return if(!opened) ViewHolderClosed(AlbumImageViewBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
)) else ViewHolderOpen(OpenedAlbumBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
), clickCallback!!)
|
||||
))
|
||||
}
|
||||
|
||||
override fun getItemCount() = media_attachments.size
|
||||
@ -839,6 +861,24 @@ class AlbumViewPagerAdapter(
|
||||
setDoubleTapZoomDpi(240)
|
||||
resetScaleAndCenter()
|
||||
}
|
||||
holder.image.setOnClickListener {
|
||||
val windowInsetsController = WindowCompat.getInsetsController((it.context as Activity).window, it)
|
||||
// Configure the behavior of the hidden system bars
|
||||
if (isActionBarHidden) {
|
||||
windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
// Hide both the status bar and the navigation bar
|
||||
(it.context as AppCompatActivity).supportActionBar?.show()
|
||||
windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
|
||||
isActionBarHidden = false
|
||||
} else {
|
||||
// Configure the behavior of the hidden system bars
|
||||
windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
// Hide both the status bar and the navigation bar
|
||||
(it.context as AppCompatActivity).supportActionBar?.hide()
|
||||
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
|
||||
isActionBarHidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
else Glide.with(holder.binding.root)
|
||||
.asDrawable().fitCenter()
|
||||
@ -884,13 +924,9 @@ class AlbumViewPagerAdapter(
|
||||
abstract val videoPlayButton: ImageView
|
||||
}
|
||||
|
||||
class ViewHolderOpen(override val binding: OpenedAlbumBinding, clickCallback: () -> Unit) : ViewHolder(binding) {
|
||||
class ViewHolderOpen(override val binding: OpenedAlbumBinding) : ViewHolder(binding) {
|
||||
override val image: SubsamplingScaleImageView = binding.imageImageView
|
||||
override val videoPlayButton: ImageView = binding.videoPlayButton
|
||||
|
||||
init {
|
||||
image.setOnClickListener { clickCallback() }
|
||||
}
|
||||
}
|
||||
class ViewHolderClosed(override val binding: AlbumImageViewBinding) : ViewHolder(binding) {
|
||||
override val image: ImageView = binding.imageImageView
|
||||
|
@ -50,7 +50,7 @@ private fun showError(
|
||||
* Makes the UI respond to various [LoadState]s, including errors when an error message is shown.
|
||||
*/
|
||||
internal fun <T: Any> initAdapter(
|
||||
progressBar: ProgressBar, swipeRefreshLayout: SwipeRefreshLayout?,
|
||||
progressBar: ProgressBar, swipeRefreshLayout: SwipeRefreshLayout,
|
||||
recyclerView: RecyclerView, motionLayout: MotionLayout, errorLayout: ErrorLayoutBinding,
|
||||
adapter: PagingDataAdapter<T, RecyclerView.ViewHolder>,
|
||||
header: StoriesAdapter? = null
|
||||
@ -71,15 +71,14 @@ internal fun <T: Any> initAdapter(
|
||||
).toTypedArray()
|
||||
)
|
||||
|
||||
swipeRefreshLayout?.setOnRefreshListener {
|
||||
swipeRefreshLayout.setOnRefreshListener {
|
||||
adapter.refresh()
|
||||
adapter.notifyDataSetChanged()
|
||||
header?.refreshStories()
|
||||
}
|
||||
|
||||
adapter.addLoadStateListener { loadState ->
|
||||
|
||||
if(!progressBar.isVisible && swipeRefreshLayout?.isRefreshing == true) {
|
||||
if(!progressBar.isVisible && swipeRefreshLayout.isRefreshing) {
|
||||
// Stop loading spinner when loading is done
|
||||
swipeRefreshLayout.isRefreshing = loadState.refresh is LoadState.Loading
|
||||
}
|
||||
|
@ -1,27 +1,22 @@
|
||||
package org.pixeldroid.app.posts.feeds.cachedFeeds
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.LoadState.NotLoading
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.paging.RemoteMediator
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import org.pixeldroid.app.databinding.FragmentFeedBinding
|
||||
import org.pixeldroid.app.directmessages.BounceEdgeEffectFactory
|
||||
import org.pixeldroid.app.posts.feeds.initAdapter
|
||||
import org.pixeldroid.app.stories.StoriesAdapter
|
||||
import org.pixeldroid.app.utils.BaseFragment
|
||||
@ -60,69 +55,38 @@ open class CachedFeedFragment<T: FeedContentDatabase> : BaseFragment() {
|
||||
//TODO rename function to something that makes sense
|
||||
internal fun initSearch() {
|
||||
// Scroll to top when the list is refreshed from network.
|
||||
// lifecycleScope.launchWhenStarted {
|
||||
// adapter.loadStateFlow
|
||||
// // Only emit when REFRESH LoadState for RemoteMediator changes.
|
||||
// .distinctUntilChangedBy {
|
||||
// it.refresh
|
||||
// }
|
||||
// // Only react to cases where Remote REFRESH completes i.e., NotLoading.
|
||||
// .filter { it.refresh is NotLoading}
|
||||
// .collect { binding.list.scrollToPosition(0) }
|
||||
// }
|
||||
lifecycleScope.launchWhenStarted {
|
||||
adapter.loadStateFlow
|
||||
// Only emit when REFRESH LoadState for RemoteMediator changes.
|
||||
.distinctUntilChangedBy {
|
||||
it.refresh
|
||||
}
|
||||
// Only react to cases where Remote REFRESH completes i.e., NotLoading.
|
||||
.filter { it.refresh is NotLoading}
|
||||
.collect { binding.list.scrollToPosition(0) }
|
||||
}
|
||||
}
|
||||
|
||||
fun createView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?, reverseLayout: Boolean = false): ConstraintLayout {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
binding = FragmentFeedBinding.inflate(layoutInflater)
|
||||
|
||||
val callback: () -> Unit = {
|
||||
binding.bottomLoadingBar.visibility = View.VISIBLE
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
delay(1000) // Wait 1 second
|
||||
binding.bottomLoadingBar.visibility = View.GONE
|
||||
}
|
||||
adapter.refresh()
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
val swipeRefreshLayout = if(reverseLayout) {
|
||||
binding.swipeRefreshLayout.isEnabled = false
|
||||
binding.list.apply {
|
||||
layoutManager = LinearLayoutManager(context).apply {
|
||||
stackFromEnd = false
|
||||
this.reverseLayout = true
|
||||
}
|
||||
edgeEffectFactory = BounceEdgeEffectFactory(callback, context)
|
||||
}
|
||||
null
|
||||
} else binding.swipeRefreshLayout
|
||||
|
||||
initAdapter(binding.progressBar, swipeRefreshLayout,
|
||||
initAdapter(binding.progressBar, binding.swipeRefreshLayout,
|
||||
binding.list, binding.motionLayout, binding.errorLayout, adapter,
|
||||
headerAdapter
|
||||
)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return createView(inflater, container, savedInstanceState, false)
|
||||
}
|
||||
|
||||
fun onTabReClicked() {
|
||||
binding.list.limitedLengthSmoothScrollToPosition(0)
|
||||
}
|
||||
|
||||
private fun onPullUp() {
|
||||
// Handle the pull-up action
|
||||
Log.e("bottom", "reached")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -16,24 +16,21 @@
|
||||
|
||||
package org.pixeldroid.app.posts.feeds.cachedFeeds
|
||||
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.RemoteMediator
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.pixeldroid.app.utils.api.objects.FeedContentDatabase
|
||||
import androidx.paging.*
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.db.dao.feedContent.FeedContentDao
|
||||
import org.pixeldroid.app.utils.api.objects.FeedContentDatabase
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Repository class that works with local and remote data sources.
|
||||
*/
|
||||
class FeedContentRepository<T: FeedContentDatabase> @ExperimentalPagingApi constructor(
|
||||
class FeedContentRepository<T: FeedContentDatabase> @ExperimentalPagingApi
|
||||
@Inject constructor(
|
||||
private val db: AppDatabase,
|
||||
private val dao: FeedContentDao<T>,
|
||||
private val mediator: RemoteMediator<Int, T>,
|
||||
private val conversationsId: String = "",
|
||||
private val mediator: RemoteMediator<Int, T>
|
||||
) {
|
||||
|
||||
/**
|
||||
@ -45,7 +42,7 @@ class FeedContentRepository<T: FeedContentDatabase> @ExperimentalPagingApi const
|
||||
val user = db.userDao().getActiveUser()!!
|
||||
|
||||
val pagingSourceFactory = {
|
||||
dao.feedContent(user.user_id, user.instance_uri, conversationsId)
|
||||
dao.feedContent(user.user_id, user.instance_uri)
|
||||
}
|
||||
|
||||
return Pager(
|
||||
|
262
app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/directMessages/DirectMessagesFragment.kt
Normal file
262
app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/directMessages/DirectMessagesFragment.kt
Normal file
@ -0,0 +1,262 @@
|
||||
package org.pixeldroid.app.posts.feeds.cachedFeeds.directMessages
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.FragmentConversationsBinding
|
||||
import org.pixeldroid.app.databinding.FragmentNotificationsBinding
|
||||
import org.pixeldroid.app.posts.PostActivity
|
||||
import org.pixeldroid.app.posts.feeds.cachedFeeds.CachedFeedFragment
|
||||
import org.pixeldroid.app.posts.feeds.cachedFeeds.FeedViewModel
|
||||
import org.pixeldroid.app.posts.feeds.cachedFeeds.ViewModelFactory
|
||||
import org.pixeldroid.app.posts.parseHTMLText
|
||||
import org.pixeldroid.app.posts.setTextViewFromISO8601
|
||||
import org.pixeldroid.app.profile.ProfileActivity
|
||||
import org.pixeldroid.app.utils.api.objects.Account
|
||||
import org.pixeldroid.app.utils.api.objects.Conversation
|
||||
import org.pixeldroid.app.utils.api.objects.Notification
|
||||
import org.pixeldroid.app.utils.api.objects.Status
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
import org.pixeldroid.app.utils.notificationsWorker.makeChannelGroupId
|
||||
|
||||
|
||||
/**
|
||||
* Fragment for the notifications tab.
|
||||
*/
|
||||
class DirectMessagesFragment : CachedFeedFragment<Conversation>() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
adapter = DirectMessagesAdapter(apiHolder)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
// get the view model
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
viewModel = ViewModelProvider(
|
||||
requireActivity(),
|
||||
ViewModelFactory(db, db.directMessagesDao(), DirectMessagesRemoteMediator(apiHolder, db))
|
||||
)["conversations", FeedViewModel::class.java] as FeedViewModel<Conversation>
|
||||
|
||||
launch()
|
||||
initSearch()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
with(NotificationManagerCompat.from(requireContext())) {
|
||||
// Cancel account notification group
|
||||
db.userDao().getActiveUser()?.let {
|
||||
cancel( makeChannelGroupId(it).hashCode())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* View Holder for a [Conversation] RecyclerView list item.
|
||||
*/
|
||||
class ConversationViewHolder(binding: FragmentConversationsBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
private val messageTime: TextView = binding.messageTime
|
||||
private val avatar: ImageView = binding.notificationAvatar
|
||||
private val photoThumbnail: ImageView = binding.notificationPhotoThumbnail
|
||||
|
||||
private var conversation: Conversation? = null
|
||||
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
conversation?.openActivity()
|
||||
}
|
||||
avatar.setOnClickListener {
|
||||
val intent = conversation?.openAccountFromNotification()
|
||||
itemView.context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Conversation.openActivity() {
|
||||
val intent: Intent = openConversation()
|
||||
itemView.context.startActivity(intent)
|
||||
}
|
||||
|
||||
private fun Notification.openConversation(): Intent =
|
||||
Intent(itemView.context, PostActivity::class.java).apply {
|
||||
putExtra(Status.POST_TAG, status)
|
||||
}
|
||||
|
||||
private fun setNotificationType(
|
||||
type: Notification.NotificationType,
|
||||
username: String,
|
||||
textView: TextView
|
||||
) {
|
||||
val context = textView.context
|
||||
val (format: String, drawable: Drawable?) = when (type) {
|
||||
Notification.NotificationType.follow ->
|
||||
getStringAndDrawable(
|
||||
context,
|
||||
R.string.followed_notification,
|
||||
R.drawable.ic_follow
|
||||
)
|
||||
Notification.NotificationType.mention ->
|
||||
getStringAndDrawable(
|
||||
context,
|
||||
R.string.mention_notification,
|
||||
R.drawable.mention_at_24dp
|
||||
)
|
||||
Notification.NotificationType.comment ->
|
||||
getStringAndDrawable(
|
||||
context,
|
||||
R.string.comment_notification,
|
||||
R.drawable.ic_comment_empty
|
||||
)
|
||||
Notification.NotificationType.reblog ->
|
||||
getStringAndDrawable(
|
||||
context,
|
||||
R.string.shared_notification,
|
||||
R.drawable.ic_reblog_blue
|
||||
)
|
||||
Notification.NotificationType.favourite ->
|
||||
getStringAndDrawable(
|
||||
context,
|
||||
R.string.liked_notification,
|
||||
R.drawable.ic_like_full
|
||||
)
|
||||
Notification.NotificationType.poll ->
|
||||
getStringAndDrawable(context, R.string.poll_notification, R.drawable.poll)
|
||||
Notification.NotificationType.follow_request -> getStringAndDrawable(
|
||||
context,
|
||||
R.string.follow_request,
|
||||
R.drawable.ic_follow
|
||||
)
|
||||
Notification.NotificationType.status -> getStringAndDrawable(
|
||||
context,
|
||||
R.string.status_notification,
|
||||
R.drawable.ic_comment_empty
|
||||
)
|
||||
}
|
||||
textView.text = format.format(username)
|
||||
textView.setCompoundDrawablesWithIntrinsicBounds(
|
||||
drawable, null, null, null
|
||||
)
|
||||
}
|
||||
|
||||
private fun getStringAndDrawable(
|
||||
context: Context,
|
||||
stringToFormat: Int,
|
||||
drawable: Int
|
||||
): Pair<String, Drawable?> =
|
||||
Pair(context.getString(stringToFormat), ContextCompat.getDrawable(context, drawable))
|
||||
|
||||
|
||||
fun bind(
|
||||
conversation: Conversation?,
|
||||
api: PixelfedAPIHolder,
|
||||
lifecycleScope: LifecycleCoroutineScope,
|
||||
) {
|
||||
|
||||
this.conversation = conversation
|
||||
|
||||
Glide.with(itemView).load(conversation?.accounts?.first()?.anyAvatar()).circleCrop()
|
||||
.into(avatar)
|
||||
|
||||
val previewUrl = conversation?.accounts?.first()?.anyAvatar()
|
||||
if (!previewUrl.isNullOrBlank()) {
|
||||
Glide.with(itemView).load(previewUrl)
|
||||
.placeholder(R.drawable.ic_picture_fallback).into(photoThumbnail)
|
||||
} else {
|
||||
photoThumbnail.visibility = View.GONE
|
||||
}
|
||||
|
||||
conversation?.last_status?.created_at?.let {
|
||||
setTextViewFromISO8601(
|
||||
it,
|
||||
notificationTime,
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
// Convert HTML to clickable text
|
||||
postDescription.text =
|
||||
parseHTMLText(
|
||||
notification?.status?.content ?: "",
|
||||
notification?.status?.mentions,
|
||||
api,
|
||||
itemView.context,
|
||||
lifecycleScope
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(parent: ViewGroup): ConversationViewHolder {
|
||||
val itemBinding = FragmentNotificationsBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
)
|
||||
return ConversationViewHolder(itemBinding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
inner class DirectMessagesAdapter(
|
||||
private val apiHolder: PixelfedAPIHolder,
|
||||
) : PagingDataAdapter<Conversation, RecyclerView.ViewHolder>(
|
||||
object : DiffUtil.ItemCallback<Conversation>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: Conversation,
|
||||
newItem: Conversation
|
||||
): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: Conversation,
|
||||
newItem: Conversation
|
||||
): Boolean =
|
||||
oldItem == newItem
|
||||
}
|
||||
) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return ConversationViewHolder.create(parent)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return R.layout.fragment_notifications
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val uiModel = getItem(position)
|
||||
uiModel?.let {
|
||||
(holder as ConversationViewHolder).bind(
|
||||
it,
|
||||
apiHolder,
|
||||
lifecycleScope
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -14,16 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.pixeldroid.app.directmessages
|
||||
package org.pixeldroid.app.posts.feeds.cachedFeeds.directMessages
|
||||
|
||||
import androidx.paging.*
|
||||
import androidx.paging.PagingSource.LoadResult
|
||||
import androidx.room.withTransaction
|
||||
import org.pixeldroid.app.utils.api.objects.Conversation
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
import org.pixeldroid.app.utils.api.objects.Notification
|
||||
import retrofit2.HttpException
|
||||
import java.lang.Exception
|
||||
import java.lang.NullPointerException
|
||||
import javax.inject.Inject
|
||||
@ -42,29 +39,26 @@ class DirectMessagesRemoteMediator @Inject constructor(
|
||||
) : RemoteMediator<Int, Conversation>() {
|
||||
|
||||
override suspend fun load(loadType: LoadType, state: PagingState<Int, Conversation>): MediatorResult {
|
||||
|
||||
val maxId = when (loadType) {
|
||||
LoadType.REFRESH -> null
|
||||
LoadType.PREPEND -> {
|
||||
// No prepend for the moment, might be nice to add later
|
||||
return MediatorResult.Success(endOfPaginationReached = true)
|
||||
}
|
||||
LoadType.APPEND -> state.lastItemOrNull()?.id
|
||||
?: return MediatorResult.Success(endOfPaginationReached = true)
|
||||
}
|
||||
|
||||
try {
|
||||
val user = db.userDao().getActiveUser()
|
||||
?: return MediatorResult.Error(NullPointerException("No active user exists"))
|
||||
?: return MediatorResult.Error(NullPointerException("No active user exists"))
|
||||
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
|
||||
val nextPage = when (loadType) {
|
||||
LoadType.REFRESH -> null
|
||||
LoadType.PREPEND -> {
|
||||
// No prepend for the moment, might be nice to add later
|
||||
return MediatorResult.Success(endOfPaginationReached = true)
|
||||
}
|
||||
LoadType.APPEND -> state.lastItemOrNull()?.id?.toIntOrNull()
|
||||
?.let { state.closestPageToPosition(it) }?.nextKey
|
||||
?: return MediatorResult.Success(endOfPaginationReached = true)
|
||||
}
|
||||
|
||||
val apiResponse =
|
||||
// Pixelfed uses Laravel's paging mechanism for pagination.
|
||||
//TODO, implement also for Mastodon (see FollowersPagingSource)
|
||||
api.directMessagesList(
|
||||
limit = state.config.pageSize.toString(),
|
||||
page = nextPage
|
||||
)
|
||||
val apiResponse = api.viewAllConversations(
|
||||
max_id = maxId,
|
||||
limit = state.config.pageSize.toString()
|
||||
)
|
||||
|
||||
apiResponse.forEach{it.user_id = user.user_id; it.instance_uri = user.instance_uri}
|
||||
|
@ -1,14 +1,12 @@
|
||||
package org.pixeldroid.app.posts.feeds.cachedFeeds.postFeeds
|
||||
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.LoadType
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import androidx.paging.RemoteMediator
|
||||
import androidx.paging.*
|
||||
import androidx.room.withTransaction
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.db.entities.HomeStatusDatabaseEntity
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
import org.pixeldroid.app.utils.db.entities.HomeStatusDatabaseEntity
|
||||
import java.lang.NullPointerException
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
/**
|
||||
@ -19,7 +17,7 @@ import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
* a local db cache.
|
||||
*/
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
class HomeFeedRemoteMediator(
|
||||
class HomeFeedRemoteMediator @Inject constructor(
|
||||
private val apiHolder: PixelfedAPIHolder,
|
||||
private val db: AppDatabase,
|
||||
) : RemoteMediator<Int, HomeStatusDatabaseEntity>() {
|
||||
|
@ -40,6 +40,8 @@ class PostFeedFragment<T: FeedContentDatabase>: CachedFeedFragment<T>() {
|
||||
|
||||
home = requireArguments().getBoolean("home")
|
||||
|
||||
adapter = PostsAdapter(requireContext().displayDimensionsInPx())
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
if (home){
|
||||
mediator = HomeFeedRemoteMediator(apiHolder, db) as RemoteMediator<Int, T>
|
||||
@ -59,7 +61,6 @@ class PostFeedFragment<T: FeedContentDatabase>: CachedFeedFragment<T>() {
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View? {
|
||||
adapter = PostsAdapter(requireContext().displayDimensionsInPx())
|
||||
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
|
10
app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/PublicFeedRemoteMediator.kt
10
app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/PublicFeedRemoteMediator.kt
@ -16,15 +16,13 @@
|
||||
|
||||
package org.pixeldroid.app.posts.feeds.cachedFeeds.postFeeds
|
||||
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.LoadType
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import androidx.paging.RemoteMediator
|
||||
import androidx.paging.*
|
||||
import androidx.room.withTransaction
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.db.entities.PublicFeedStatusDatabaseEntity
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
import java.lang.NullPointerException
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* RemoteMediator for the public feed.
|
||||
@ -34,7 +32,7 @@ import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
* a local db cache.
|
||||
*/
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
class PublicFeedRemoteMediator(
|
||||
class PublicFeedRemoteMediator @Inject constructor(
|
||||
private val apiHolder: PixelfedAPIHolder,
|
||||
private val db: AppDatabase
|
||||
) : RemoteMediator<Int, PublicFeedStatusDatabaseEntity>() {
|
||||
|
@ -8,16 +8,18 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.databinding.FragmentFeedBinding
|
||||
import org.pixeldroid.app.posts.feeds.initAdapter
|
||||
import org.pixeldroid.app.posts.feeds.launch
|
||||
import org.pixeldroid.app.utils.BaseFragment
|
||||
import org.pixeldroid.app.utils.api.objects.FeedContent
|
||||
import org.pixeldroid.app.utils.limitedLengthSmoothScrollToPosition
|
||||
|
||||
|
||||
/**
|
||||
@ -28,7 +30,8 @@ open class UncachedFeedFragment<T: FeedContent> : BaseFragment() {
|
||||
internal lateinit var viewModel: FeedViewModel<T>
|
||||
internal lateinit var adapter: PagingDataAdapter<T, RecyclerView.ViewHolder>
|
||||
|
||||
var binding: FragmentFeedBinding? = null
|
||||
lateinit var binding: FragmentFeedBinding
|
||||
|
||||
|
||||
private var job: Job? = null
|
||||
|
||||
@ -38,42 +41,32 @@ open class UncachedFeedFragment<T: FeedContent> : BaseFragment() {
|
||||
}
|
||||
|
||||
internal fun initSearch() {
|
||||
// // Scroll to top when the list is refreshed from network.
|
||||
// lifecycleScope.launch {
|
||||
// adapter.loadStateFlow
|
||||
// // Only emit when REFRESH LoadState for RemoteMediator changes.
|
||||
// .distinctUntilChangedBy { it.refresh }
|
||||
// // Only react to cases where Remote REFRESH completes i.e., NotLoading.
|
||||
// .filter { it.refresh is LoadState.NotLoading }
|
||||
// .collect { binding?.list?.scrollToPosition(0) }
|
||||
// }
|
||||
}
|
||||
|
||||
fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?, swipeRefreshLayout: SwipeRefreshLayout?
|
||||
): View {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
binding = FragmentFeedBinding.inflate(layoutInflater)
|
||||
|
||||
binding!!.let {
|
||||
initAdapter(
|
||||
it.progressBar, swipeRefreshLayout ?: it.swipeRefreshLayout, it.list,
|
||||
it.motionLayout, it.errorLayout, adapter
|
||||
)
|
||||
|
||||
// Scroll to top when the list is refreshed from network.
|
||||
lifecycleScope.launch {
|
||||
adapter.loadStateFlow
|
||||
// Only emit when REFRESH LoadState for RemoteMediator changes.
|
||||
.distinctUntilChangedBy { it.refresh }
|
||||
// Only react to cases where Remote REFRESH completes i.e., NotLoading.
|
||||
.filter { it.refresh is LoadState.NotLoading }
|
||||
.collect { binding.list.scrollToPosition(0) }
|
||||
}
|
||||
return binding!!.root
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return onCreateView(inflater, container, savedInstanceState, null)
|
||||
}
|
||||
fun onTabReClicked() {
|
||||
binding?.list?.limitedLengthSmoothScrollToPosition(0)
|
||||
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
binding = FragmentFeedBinding.inflate(layoutInflater)
|
||||
|
||||
initAdapter(
|
||||
binding.progressBar, binding.swipeRefreshLayout, binding.list,
|
||||
binding.motionLayout, binding.errorLayout, adapter
|
||||
)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,15 +4,11 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.posts.StatusViewHolder
|
||||
import org.pixeldroid.app.posts.feeds.UIMODEL_STATUS_COMPARATOR
|
||||
@ -23,7 +19,6 @@ import org.pixeldroid.app.utils.api.objects.Status
|
||||
import org.pixeldroid.app.utils.api.objects.Tag.Companion.HASHTAG_TAG
|
||||
import org.pixeldroid.app.utils.displayDimensionsInPx
|
||||
|
||||
|
||||
/**
|
||||
* Fragment to show a list of [Status]es, as a result of a search or a hashtag.
|
||||
*/
|
||||
@ -38,7 +33,7 @@ class UncachedPostsFragment : UncachedFeedFragment<Status>() {
|
||||
|
||||
hashtagOrQuery = arguments?.getString(HASHTAG_TAG)
|
||||
|
||||
if (hashtagOrQuery == null) {
|
||||
if(hashtagOrQuery == null){
|
||||
search = true
|
||||
hashtagOrQuery = arguments?.getString("searchFeed")!!
|
||||
}
|
||||
|
@ -5,15 +5,12 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.NestedScrollingChild
|
||||
import androidx.core.view.NestedScrollingChildHelper
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.CommentBinding
|
||||
import org.pixeldroid.app.posts.PostActivity
|
||||
@ -28,7 +25,7 @@ import org.pixeldroid.app.utils.setProfileImageFromURL
|
||||
/**
|
||||
* Fragment to show a list of [Status]s, in form of comments
|
||||
*/
|
||||
class CommentFragment: UncachedFeedFragment<Status>() {
|
||||
class CommentFragment : UncachedFeedFragment<Status>() {
|
||||
|
||||
private lateinit var id: String
|
||||
private lateinit var domain: String
|
||||
@ -45,13 +42,11 @@ class CommentFragment: UncachedFeedFragment<Status>() {
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
|
||||
val view = super.onCreateView(
|
||||
inflater, container, savedInstanceState,
|
||||
(activity as? PostActivity)?.binding?.swipeRefreshLayout
|
||||
)
|
||||
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
// Get the view model
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@ -67,7 +62,6 @@ class CommentFragment: UncachedFeedFragment<Status>() {
|
||||
launch()
|
||||
initSearch()
|
||||
|
||||
binding?.swipeRefreshLayout?.isEnabled = false
|
||||
return view
|
||||
}
|
||||
companion object {
|
||||
|
@ -1,8 +1,6 @@
|
||||
package org.pixeldroid.app.posts.feeds.uncachedFeeds.hashtags
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.fragment.app.replace
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityFollowersBinding
|
||||
import org.pixeldroid.app.posts.feeds.uncachedFeeds.UncachedPostsFragment
|
||||
@ -11,8 +9,10 @@ import org.pixeldroid.app.utils.api.objects.Tag.Companion.HASHTAG_TAG
|
||||
|
||||
|
||||
class HashTagActivity : BaseActivity() {
|
||||
private var tagFragment = UncachedPostsFragment()
|
||||
private lateinit var binding: ActivityFollowersBinding
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityFollowersBinding.inflate(layoutInflater)
|
||||
@ -32,10 +32,10 @@ class HashTagActivity : BaseActivity() {
|
||||
|
||||
val arguments = Bundle()
|
||||
arguments.putSerializable(HASHTAG_TAG, tag)
|
||||
tagFragment.arguments = arguments
|
||||
|
||||
supportFragmentManager.beginTransaction()
|
||||
.add(R.id.followsFragment, tagFragment).commit()
|
||||
|
||||
supportFragmentManager.commit {
|
||||
setReorderingAllowed(true)
|
||||
replace<UncachedPostsFragment>(R.id.conversationFragment, args = arguments)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -87,7 +87,7 @@ class CollectionActivity : BaseActivity() {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
// Relaunch same activity, to avoid duplicates in history
|
||||
super.onNewIntent(intent)
|
||||
finish()
|
||||
|
@ -25,7 +25,7 @@ import org.pixeldroid.app.utils.openUrl
|
||||
|
||||
class EditProfileActivity : BaseActivity() {
|
||||
|
||||
private val model: EditProfileViewModel by viewModels()
|
||||
private lateinit var model: EditProfileViewModel
|
||||
private lateinit var binding: ActivityEditProfileBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@ -35,6 +35,9 @@ class EditProfileActivity : BaseActivity() {
|
||||
setSupportActionBar(binding.topBar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
val _model: EditProfileViewModel by viewModels { EditProfileViewModelFactory(application) }
|
||||
model = _model
|
||||
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
// Handle the back button event
|
||||
if(model.madeChanges()){
|
||||
@ -42,38 +45,36 @@ class EditProfileActivity : BaseActivity() {
|
||||
setMessage(getString(R.string.profile_save_changes))
|
||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
this@addCallback.remove()
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
this@addCallback.isEnabled = false
|
||||
super.onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
}.show()
|
||||
} else {
|
||||
this@addCallback.remove()
|
||||
if (model.submittedChanges) setResult(RESULT_OK)
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
this.isEnabled = false
|
||||
super.onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
model.uiState.collect { uiState ->
|
||||
if(binding.bioEditText.text.toString() != uiState.bio) binding.bioEditText.setText(uiState.bio)
|
||||
if(binding.nameEditText.text.toString() != uiState.name) binding.nameEditText.setText(uiState.name)
|
||||
|
||||
binding.progressCard.visibility = if(uiState.loadingProfile || uiState.sendingProfile || uiState.uploadingPicture || uiState.profileSent || uiState.error) View.VISIBLE else View.INVISIBLE
|
||||
|
||||
if(uiState.profileLoaded){
|
||||
binding.bioEditText.setText(uiState.bio)
|
||||
binding.nameEditText.setText(uiState.name)
|
||||
model.changesApplied()
|
||||
}
|
||||
binding.progressCard.visibility = if(uiState.loadingProfile || uiState.sendingProfile || uiState.profileSent || uiState.error) View.VISIBLE else View.INVISIBLE
|
||||
if(uiState.loadingProfile) binding.progressText.setText(R.string.fetching_profile)
|
||||
else if(uiState.sendingProfile) binding.progressText.setText(R.string.saving_profile)
|
||||
|
||||
binding.privateSwitch.isChecked = uiState.privateAccount == true
|
||||
Glide.with(binding.profilePic).load(uiState.profilePictureUri)
|
||||
.apply(RequestOptions.circleCropTransform())
|
||||
.into(binding.profilePic)
|
||||
|
||||
binding.savingProgressBar.visibility =
|
||||
if(uiState.error || (uiState.profileSent && !uiState.uploadingPicture)) View.GONE
|
||||
else View.VISIBLE
|
||||
binding.savingProgressBar.visibility = if(uiState.error || uiState.profileSent) View.GONE
|
||||
else View.VISIBLE
|
||||
|
||||
if(uiState.profileSent && !uiState.uploadingPicture && !uiState.error){
|
||||
if(uiState.profileSent){
|
||||
binding.progressText.setText(R.string.profile_saved)
|
||||
binding.done.visibility = View.VISIBLE
|
||||
} else {
|
||||
@ -111,18 +112,18 @@ class EditProfileActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
binding.profilePic.setOnClickListener {
|
||||
Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
type = "*/*"
|
||||
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*"))
|
||||
action = Intent.ACTION_GET_CONTENT
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false)
|
||||
uploadImageResultContract.launch(
|
||||
Intent.createChooser(this, null)
|
||||
)
|
||||
}
|
||||
}
|
||||
// binding.changeImageButton.setOnClickListener {
|
||||
// Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
// type = "*/*"
|
||||
// putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*"))
|
||||
// action = Intent.ACTION_GET_CONTENT
|
||||
// addCategory(Intent.CATEGORY_OPENABLE)
|
||||
// putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false)
|
||||
// uploadImageResultContract.launch(
|
||||
// Intent.createChooser(this, null)
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
private val uploadImageResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
@ -136,10 +137,10 @@ class EditProfileActivity : BaseActivity() {
|
||||
val imageUri: String = clipData.getItemAt(i).uri.toString()
|
||||
images.add(imageUri)
|
||||
}
|
||||
model.updateImage(images.first())
|
||||
model.uploadImage(images.first())
|
||||
} else if (data.data != null) {
|
||||
images.add(data.data!!.toString())
|
||||
model.updateImage(images.first())
|
||||
model.uploadImage(images.first())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,16 @@
|
||||
package org.pixeldroid.app.profile
|
||||
|
||||
import android.content.Context
|
||||
import android.app.Application
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import android.text.Editable
|
||||
import android.util.Log
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
@ -21,33 +21,23 @@ import kotlinx.coroutines.launch
|
||||
import okhttp3.MultipartBody
|
||||
import org.pixeldroid.app.postCreation.ProgressRequestBody
|
||||
import org.pixeldroid.app.posts.fromHtml
|
||||
import org.pixeldroid.app.utils.PixelDroidApplication
|
||||
import org.pixeldroid.app.utils.api.objects.Account
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.db.updateUserInfoDb
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
import retrofit2.HttpException
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class EditProfileViewModel @Inject constructor(
|
||||
@ApplicationContext private val applicationContext: Context
|
||||
): ViewModel() {
|
||||
class EditProfileViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
@Inject
|
||||
lateinit var apiHolder: PixelfedAPIHolder
|
||||
|
||||
@Inject
|
||||
lateinit var db: AppDatabase
|
||||
|
||||
private val _uiState = MutableStateFlow(EditProfileActivityUiState())
|
||||
val uiState: StateFlow<EditProfileActivityUiState> = _uiState
|
||||
|
||||
private var oldProfile: Account? = null
|
||||
|
||||
var submittedChanges = false
|
||||
private set
|
||||
var oldProfile: Account? = null
|
||||
|
||||
init {
|
||||
(application as PixelDroidApplication).getAppComponent().inject(this)
|
||||
loadProfile()
|
||||
}
|
||||
|
||||
@ -56,7 +46,6 @@ class EditProfileViewModel @Inject constructor(
|
||||
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
try {
|
||||
val profile = api.verifyCredentials()
|
||||
updateUserInfoDb(db, profile)
|
||||
if (oldProfile == null) oldProfile = profile
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
@ -87,10 +76,15 @@ class EditProfileViewModel @Inject constructor(
|
||||
fun sendProfile() {
|
||||
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
|
||||
val requestBody =
|
||||
null //MultipartBody.Part.createFormData("avatar", System.currentTimeMillis().toString(), avatarBody)
|
||||
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
sendingProfile = true,
|
||||
profileSent = false,
|
||||
loadingProfile = false,
|
||||
profileLoaded = false,
|
||||
error = false
|
||||
)
|
||||
}
|
||||
@ -103,17 +97,12 @@ class EditProfileViewModel @Inject constructor(
|
||||
note = bio,
|
||||
locked = privateAccount,
|
||||
)
|
||||
if (madeChanges()) submittedChanges = true
|
||||
oldProfile = account
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
bio = account.source?.note
|
||||
?: account.note?.let { fromHtml(it).toString() },
|
||||
bio = account.source?.note ?: account.note?.let {fromHtml(it).toString()},
|
||||
name = account.display_name,
|
||||
profilePictureUri = if (profilePictureChanged) profilePictureUri
|
||||
else account.anyAvatar()?.toUri(),
|
||||
uploadProgress = 0,
|
||||
uploadingPicture = profilePictureChanged,
|
||||
profilePictureUri = account.anyAvatar()?.toUri(),
|
||||
privateAccount = account.locked,
|
||||
sendingProfile = false,
|
||||
profileSent = true,
|
||||
@ -122,13 +111,14 @@ class EditProfileViewModel @Inject constructor(
|
||||
error = false
|
||||
)
|
||||
}
|
||||
if(profilePictureChanged) uploadImage()
|
||||
} catch (exception: Exception) {
|
||||
Log.e("TAG", exception.toString())
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
sendingProfile = false,
|
||||
profileSent = false,
|
||||
loadingProfile = false,
|
||||
profileLoaded = false,
|
||||
error = true
|
||||
)
|
||||
}
|
||||
@ -155,16 +145,20 @@ class EditProfileViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun changesApplied() {
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(profileLoaded = false)
|
||||
}
|
||||
}
|
||||
|
||||
fun madeChanges(): Boolean =
|
||||
with(uiState.value) {
|
||||
val privateChanged = oldProfile?.locked != privateAccount
|
||||
val displayNameChanged = oldProfile?.display_name != name
|
||||
val bioChanged: Boolean = oldProfile?.source?.note?.let { it != bio }
|
||||
// If source note is null, check note
|
||||
val bioUnchanged: Boolean = oldProfile?.source?.note?.let { it != bio }
|
||||
// If source note is null, check note
|
||||
?: oldProfile?.note?.let { fromHtml(it).toString() != bio }
|
||||
?: true
|
||||
|
||||
profilePictureChanged || privateChanged || displayNameChanged || bioChanged
|
||||
oldProfile?.locked != privateAccount || oldProfile?.display_name != name
|
||||
|| bioUnchanged
|
||||
}
|
||||
|
||||
fun clickedCard() {
|
||||
@ -184,27 +178,16 @@ class EditProfileViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun updateImage(image: String) {
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
profilePictureUri = image.toUri(),
|
||||
profilePictureChanged = true,
|
||||
profileSent = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun uploadImage() {
|
||||
val image = uiState.value.profilePictureUri!!
|
||||
|
||||
fun uploadImage(image: String) {
|
||||
//TODO fix
|
||||
val inputStream =
|
||||
applicationContext.contentResolver.openInputStream(image)
|
||||
getApplication<PixelDroidApplication>().contentResolver.openInputStream(image.toUri())
|
||||
?: return
|
||||
|
||||
val size: Long =
|
||||
if (image.scheme == "content") {
|
||||
applicationContext.contentResolver.query(
|
||||
image,
|
||||
if (image.toUri().scheme == "content") {
|
||||
getApplication<PixelDroidApplication>().contentResolver.query(
|
||||
image.toUri(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
@ -220,7 +203,7 @@ class EditProfileViewModel @Inject constructor(
|
||||
cursor.getLong(sizeIndex)
|
||||
} ?: 0
|
||||
} else {
|
||||
image.toFile().length()
|
||||
image.toUri().toFile().length()
|
||||
}
|
||||
|
||||
val imagePart = ProgressRequestBody(inputStream, size, "image/*")
|
||||
@ -242,32 +225,21 @@ class EditProfileViewModel @Inject constructor(
|
||||
var postSub: Disposable? = null
|
||||
|
||||
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
|
||||
val pixelfed = db.instanceDao().getActiveInstance().pixelfed
|
||||
|
||||
val inter =
|
||||
if(pixelfed) api.updateProfilePicture(requestBody.parts[0])
|
||||
else api.updateProfilePictureMastodon(requestBody.parts[0])
|
||||
val inter = api.updateProfilePicture(requestBody.parts[0])
|
||||
|
||||
postSub = inter
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
/* onNext = */ { account: Account ->
|
||||
account.anyAvatar()?.let {
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
profilePictureUri = it.toUri()
|
||||
)
|
||||
}
|
||||
}
|
||||
{ it: Account ->
|
||||
Log.e("qsdfqsdfs", it.toString())
|
||||
|
||||
},
|
||||
/* onError = */ { e: Throwable ->
|
||||
Log.e("error", (e as? HttpException)?.message().orEmpty())
|
||||
{ e: Throwable ->
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
uploadProgress = 0,
|
||||
uploadingPicture = false,
|
||||
uploadingPicture = true,
|
||||
error = true
|
||||
)
|
||||
}
|
||||
@ -275,10 +247,9 @@ class EditProfileViewModel @Inject constructor(
|
||||
postSub?.dispose()
|
||||
sub.dispose()
|
||||
},
|
||||
/* onComplete = */ {
|
||||
{
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
profilePictureChanged = false,
|
||||
uploadProgress = 100,
|
||||
uploadingPicture = false
|
||||
)
|
||||
@ -294,8 +265,7 @@ class EditProfileViewModel @Inject constructor(
|
||||
data class EditProfileActivityUiState(
|
||||
val name: String? = null,
|
||||
val bio: String? = null,
|
||||
val profilePictureUri: Uri? = null,
|
||||
val profilePictureChanged: Boolean = false,
|
||||
val profilePictureUri: Uri?= null,
|
||||
val privateAccount: Boolean? = null,
|
||||
val loadingProfile: Boolean = true,
|
||||
val profileLoaded: Boolean = false,
|
||||
@ -304,4 +274,10 @@ data class EditProfileActivityUiState(
|
||||
val error: Boolean = false,
|
||||
val uploadingPicture: Boolean = false,
|
||||
val uploadProgress: Int = 0,
|
||||
)
|
||||
)
|
||||
|
||||
class EditProfileViewModelFactory(val application: Application) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.getConstructor(Application::class.java).newInstance(application)
|
||||
}
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
package org.pixeldroid.app.profile
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.fragment.app.replace
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityFollowersBinding
|
||||
import org.pixeldroid.app.posts.feeds.uncachedFeeds.accountLists.AccountListFragment
|
||||
@ -14,8 +12,10 @@ import org.pixeldroid.app.utils.api.objects.Account.Companion.FOLLOWERS_TAG
|
||||
|
||||
|
||||
class FollowsActivity : BaseActivity() {
|
||||
private var followsFragment = AccountListFragment()
|
||||
private lateinit var binding: ActivityFollowersBinding
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityFollowersBinding.inflate(layoutInflater)
|
||||
@ -47,10 +47,10 @@ class FollowsActivity : BaseActivity() {
|
||||
val arguments = Bundle()
|
||||
arguments.putSerializable(ACCOUNT_ID_TAG, id)
|
||||
arguments.putSerializable(FOLLOWERS_TAG, followers)
|
||||
followsFragment.arguments = arguments
|
||||
|
||||
supportFragmentManager.beginTransaction()
|
||||
.add(R.id.followsFragment, followsFragment).commit()
|
||||
|
||||
supportFragmentManager.commit {
|
||||
setReorderingAllowed(true)
|
||||
replace<AccountListFragment>(R.id.conversationFragment, args = arguments)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,28 +6,21 @@ import android.text.method.LinkMovementMethod
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityProfileBinding
|
||||
import org.pixeldroid.app.posts.feeds.cachedFeeds.CachedFeedFragment
|
||||
import org.pixeldroid.app.posts.feeds.uncachedFeeds.UncachedFeedFragment
|
||||
import org.pixeldroid.app.posts.parseHTMLText
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.api.PixelfedAPI
|
||||
import org.pixeldroid.app.utils.api.objects.Account
|
||||
import org.pixeldroid.app.utils.api.objects.FeedContent
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.updateUserInfoDb
|
||||
import org.pixeldroid.app.utils.setProfileImageFromURL
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
@ -61,32 +54,9 @@ class ProfileActivity : BaseActivity() {
|
||||
val tabs = createProfileTabs(account)
|
||||
setupTabs(tabs)
|
||||
setContent(account)
|
||||
|
||||
binding.profileMotion.setTransitionListener(
|
||||
object : MotionLayout.TransitionListener {
|
||||
override fun onTransitionStarted(
|
||||
motionLayout: MotionLayout?, startId: Int, endId: Int,
|
||||
) {}
|
||||
|
||||
override fun onTransitionChange(p0: MotionLayout?, p1: Int, p2: Int, p3: Float) {}
|
||||
|
||||
override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) {
|
||||
if (currentId == R.id.hideProfile && motionLayout?.startState == R.id.start) {
|
||||
// If the 1st transition has been made go to the second one
|
||||
motionLayout.setTransition(R.id.second)
|
||||
} else if(currentId == R.id.hideProfile && motionLayout?.startState == R.id.hideProfile){
|
||||
motionLayout.setTransition(R.id.first)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTransitionTrigger(
|
||||
motionLayout: MotionLayout?, triggerId: Int, positive: Boolean, progress: Float,
|
||||
) {}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun createProfileTabs(account: Account?): Array<UncachedFeedFragment<FeedContent>> {
|
||||
private fun createProfileTabs(account: Account?): Array<Fragment>{
|
||||
|
||||
val profileFeedFragment = ProfileFeedFragment()
|
||||
profileFeedFragment.arguments = Bundle().apply {
|
||||
@ -110,7 +80,7 @@ class ProfileActivity : BaseActivity() {
|
||||
putSerializable(ProfileFeedFragment.COLLECTIONS, true)
|
||||
}
|
||||
|
||||
val returnArray: Array<UncachedFeedFragment<FeedContent>> = arrayOf(
|
||||
val returnArray: Array<Fragment> = arrayOf(
|
||||
profileGridFragment,
|
||||
profileFeedFragment,
|
||||
profileCollectionsFragment
|
||||
@ -130,7 +100,7 @@ class ProfileActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
private fun setupTabs(
|
||||
tabs: Array<UncachedFeedFragment<FeedContent>>,
|
||||
tabs: Array<Fragment>
|
||||
){
|
||||
binding.viewPager.adapter = object : FragmentStateAdapter(this) {
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
@ -162,16 +132,9 @@ class ProfileActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
}.attach()
|
||||
|
||||
binding.profileTabs.addOnTabSelectedListener(object : OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab) {}
|
||||
override fun onTabUnselected(tab: TabLayout.Tab) {}
|
||||
override fun onTabReselected(tab: TabLayout.Tab) {
|
||||
tabs[tab.position].onTabReClicked()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
private fun setContent(account: Account?) {
|
||||
if(account != null) {
|
||||
setViews(account)
|
||||
@ -189,9 +152,6 @@ class ProfileActivity : BaseActivity() {
|
||||
).show()
|
||||
return@launchWhenResumed
|
||||
}
|
||||
|
||||
updateUserInfoDb(db, myAccount)
|
||||
|
||||
setViews(myAccount)
|
||||
}
|
||||
}
|
||||
@ -257,15 +217,9 @@ class ProfileActivity : BaseActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
private val editResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
// Profile was edited, reload
|
||||
setContent(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onClickEditButton() {
|
||||
editResult.launch(Intent(this, EditProfileActivity::class.java))
|
||||
val intent = Intent(this, EditProfileActivity::class.java)
|
||||
ContextCompat.startActivity(this, intent, null)
|
||||
}
|
||||
|
||||
private fun onClickFollowers(account: Account?) {
|
||||
|
@ -101,7 +101,7 @@ class ProfileFeedFragment : UncachedFeedFragment<FeedContent>() {
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
if(grid || bookmarks || collections || addCollection) {
|
||||
binding?.list?.layoutManager = GridLayoutManager(context, 3)
|
||||
binding.list.layoutManager = GridLayoutManager(context, 3)
|
||||
}
|
||||
|
||||
// Get the view model
|
||||
@ -191,11 +191,8 @@ class ProfileFeedFragment : UncachedFeedFragment<FeedContent>() {
|
||||
val url = "$domain/i/collections/create"
|
||||
|
||||
if(domain.isNullOrEmpty() || !requireContext().openUrl(url)) {
|
||||
binding?.let { binding ->
|
||||
Snackbar.make(
|
||||
binding.root, getString(R.string.new_collection_link_failed),
|
||||
Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
Snackbar.make(binding.root, getString(R.string.new_collection_link_failed),
|
||||
Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,257 +0,0 @@
|
||||
package org.pixeldroid.app.settings
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.getkeepsafe.taptargetview.TapTarget
|
||||
import com.getkeepsafe.taptargetview.TapTargetView
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.checkbox.MaterialCheckBox
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.LayoutTabsArrangeBinding
|
||||
import org.pixeldroid.app.utils.Tab
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.db.entities.TabsDatabaseEntity
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ArrangeTabsFragment: DialogFragment() {
|
||||
|
||||
private lateinit var binding: LayoutTabsArrangeBinding
|
||||
|
||||
@Inject
|
||||
lateinit var db: AppDatabase
|
||||
|
||||
private val model: ArrangeTabsViewModel by viewModels()
|
||||
|
||||
var showTutorial = false
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
|
||||
binding = LayoutTabsArrangeBinding.inflate(layoutInflater)
|
||||
|
||||
val itemCount = model.initTabsChecked()
|
||||
model.initTabsButtons(itemCount, requireContext())
|
||||
|
||||
val listFeed: RecyclerView = binding.tabs
|
||||
val listAdapter = ListViewAdapter(model)
|
||||
listFeed.adapter = listAdapter
|
||||
listFeed.layoutManager = LinearLayoutManager(requireActivity())
|
||||
val callback = object: ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) {
|
||||
override fun onMove(
|
||||
recyclerView: RecyclerView,
|
||||
source: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
listAdapter.onItemMove(source.bindingAdapterPosition, target.bindingAdapterPosition)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
// Do nothing, all items should remain in the list
|
||||
}
|
||||
}
|
||||
val itemTouchHelper = ItemTouchHelper(callback)
|
||||
itemTouchHelper.attachToRecyclerView(listFeed)
|
||||
|
||||
val dialog = MaterialAlertDialogBuilder(requireContext()).apply {
|
||||
setIcon(R.drawable.outline_bottom_navigation)
|
||||
setTitle(R.string.arrange_tabs_summary)
|
||||
setView(binding.root)
|
||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
// Save values into preferences
|
||||
val tabsChecked = listAdapter.model.uiState.value.tabsChecked.toList()
|
||||
val tabsDbEntity = tabsChecked.mapIndexed { index, (tab, checked) -> with (db.userDao().getActiveUser()!!) {
|
||||
TabsDatabaseEntity(index, user_id, instance_uri, tab.name, checked, tab.filter)
|
||||
} }
|
||||
lifecycleScope.launch {
|
||||
db.tabsDao().clearAndRefill(tabsDbEntity, model.uiState.value.userId, model.uiState.value.instanceUri)
|
||||
}
|
||||
}
|
||||
}.create()
|
||||
if (showTutorial) showTutorial(dialog)
|
||||
return dialog
|
||||
}
|
||||
|
||||
private fun showTutorial(dialog: Dialog){
|
||||
lifecycleScope.launch {
|
||||
var handle = binding.tabs.findViewHolderForLayoutPosition(0)?.itemView?.findViewById<ImageView>(R.id.dragHandle)
|
||||
while (handle == null) {
|
||||
handle = binding.tabs.findViewHolderForLayoutPosition(0)?.itemView?.findViewById(R.id.dragHandle)
|
||||
delay(100)
|
||||
}
|
||||
TapTargetView.showFor(
|
||||
dialog,
|
||||
TapTarget.forView(handle, getString(R.string.drag_customtabs_tutorial))
|
||||
.transparentTarget(true)
|
||||
.targetRadius(60), // Specify the target radius (in dp)
|
||||
object : TapTargetView.Listener() {
|
||||
// The listener can listen for regular clicks, long clicks or cancels
|
||||
override fun onTargetClick(view: TapTargetView?) {
|
||||
super.onTargetClick(view) // This call is optional
|
||||
// Perform action for the current target
|
||||
val checkBox = binding.tabs.findViewHolderForLayoutPosition(0)?.itemView?.findViewById<View>(R.id.checkBox)
|
||||
TapTargetView.showFor(
|
||||
dialog,
|
||||
TapTarget.forView(checkBox,
|
||||
getString(R.string.de_activate_tabs_tutorial))
|
||||
.transparentTarget(true)
|
||||
.targetRadius(60), // Specify the target radius (in dp)
|
||||
object : TapTargetView.Listener() {
|
||||
// The listener can listen for regular clicks, long clicks or cancels
|
||||
override fun onTargetClick(view: TapTargetView?) {
|
||||
super.onTargetClick(view) // This call is optional
|
||||
// Perform action for the current target
|
||||
val index = (Tab.defaultTabs + Tab.otherTabs).size - 1
|
||||
binding.tabs.scrollToPosition(index)
|
||||
lifecycleScope.launch {
|
||||
var hashtag =
|
||||
binding.tabs.findViewHolderForLayoutPosition(index)?.itemView?.findViewById<View>(
|
||||
R.id.textView
|
||||
)
|
||||
while (hashtag == null) {
|
||||
hashtag =
|
||||
binding.tabs.findViewHolderForLayoutPosition(index)?.itemView?.findViewById(
|
||||
R.id.textView
|
||||
)
|
||||
delay(100)
|
||||
}
|
||||
TapTargetView.showFor(
|
||||
dialog,
|
||||
TapTarget.forView(
|
||||
hashtag,
|
||||
getString(R.string.custom_feed_tutorial_title),
|
||||
getString(R.string.custom_feed_tutorial_explanation)
|
||||
)
|
||||
.transparentTarget(true)
|
||||
.targetRadius(60), // Specify the target radius (in dp)
|
||||
object : TapTargetView.Listener() {
|
||||
// The listener can listen for regular clicks, long clicks or cancels
|
||||
override fun onTargetClick(view: TapTargetView?) {
|
||||
super.onTargetClick(view) // This call is optional
|
||||
// Perform action for the current target
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
inner class ListViewAdapter(val model: ArrangeTabsViewModel):
|
||||
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val view = FrameLayout.inflate(context, R.layout.layout_tab, null)
|
||||
|
||||
// Make sure the layout occupies full width
|
||||
view.layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
|
||||
return object: RecyclerView.ViewHolder(view) {}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val textView: MaterialButton = holder.itemView.findViewById(R.id.textView)
|
||||
val checkBox: MaterialCheckBox = holder.itemView.findViewById(R.id.checkBox)
|
||||
val dragHandle: ImageView = holder.itemView.findViewById(R.id.dragHandle)
|
||||
|
||||
val tabText = model.getTabsButtons(position)
|
||||
|
||||
// Set content of each entry
|
||||
if (tabText != null) {
|
||||
textView.text = tabText
|
||||
} else {
|
||||
model.updateTabsButtons(position, model.uiState.value.tabsChecked[position].first.toLanguageString(requireContext(), db, position, true))
|
||||
textView.text = model.getTabsButtons(position)
|
||||
}
|
||||
checkBox.isChecked = model.uiState.value.tabsChecked[position].second
|
||||
|
||||
fun callbackOnClickListener(tabNew: Tab, isCheckedNew: Boolean, hashtag: String? = null) {
|
||||
tabNew.filter = hashtag?.split("#")?.filter { it.isNotBlank() }?.get(0)?.trim()
|
||||
model.tabsCheckReplace(position, Pair(tabNew, isCheckedNew))
|
||||
checkBox.isChecked = model.uiState.value.tabsChecked[position].second
|
||||
|
||||
val hashtagWithHashtag = tabNew.filter?.let {
|
||||
StringBuilder("#").append(it).toString()
|
||||
}
|
||||
|
||||
// Disable OK button when no tab is selected or when strictly more than 5 tabs are selected
|
||||
val maxItemCount = BottomNavigationView(requireContext()).maxItemCount // = 5
|
||||
(requireDialog() as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled =
|
||||
with (model.uiState.value.tabsChecked.count { (_, v) -> v }) { this in 1..maxItemCount}
|
||||
model.updateTabsButtons(position, hashtagWithHashtag ?: model.uiState.value.tabsChecked[position].first.toLanguageString(requireContext(), db, position, false))
|
||||
textView.text = model.getTabsButtons(position)
|
||||
}
|
||||
|
||||
// Also interact with checkbox when button is clicked
|
||||
textView.setOnClickListener {
|
||||
val isCheckedNew = !model.uiState.value.tabsChecked[position].second
|
||||
val tabNew = model.uiState.value.tabsChecked[position].first
|
||||
|
||||
if (tabNew == Tab.HASHTAG_FEED && isCheckedNew) {
|
||||
// Ask which hashtag should filter
|
||||
val textField = EditText(requireContext())
|
||||
|
||||
textField.hint = "hashtag"
|
||||
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(getString(R.string.feed_hashtag))
|
||||
.setMessage(getString(R.string.feed_hashtag_description))
|
||||
.setView(textField)
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val hashtag = textField.text.toString()
|
||||
callbackOnClickListener(tabNew, isCheckedNew, hashtag)
|
||||
}
|
||||
.show()
|
||||
} else {
|
||||
callbackOnClickListener(tabNew, isCheckedNew)
|
||||
}
|
||||
}
|
||||
|
||||
// Also highlight button when checkbox is clicked
|
||||
checkBox.setOnTouchListener { _, motionEvent ->
|
||||
textView.dispatchTouchEvent(motionEvent)
|
||||
}
|
||||
|
||||
// Do not highlight the button when the drag handle is touched
|
||||
dragHandle.setOnTouchListener { _, _ -> true }
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return model.uiState.value.tabsChecked.size
|
||||
}
|
||||
|
||||
fun onItemMove(from: Int, to: Int) {
|
||||
val previous = model.tabsCheckedRemove(from)
|
||||
model.tabsCheckedAdd(to, previous)
|
||||
model.swapTabsButtons(from, to)
|
||||
notifyItemMoved(from, to)
|
||||
notifyItemChanged(to) // necessary to avoid checkBox issues
|
||||
}
|
||||
}
|
||||
}
|
@ -1,141 +0,0 @@
|
||||
package org.pixeldroid.app.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.EditText
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.pixeldroid.app.utils.Tab
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.db.entities.TabsDatabaseEntity
|
||||
import org.pixeldroid.app.utils.loadDbMenuTabs
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ArrangeTabsViewModel @Inject constructor(
|
||||
private val db: AppDatabase
|
||||
): ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(ArrangeTabsUiState())
|
||||
val uiState: StateFlow<ArrangeTabsUiState> = _uiState
|
||||
|
||||
private var oldTabsChecked: MutableList<Pair<Tab, Boolean>> = mutableListOf()
|
||||
private var oldTabsButtons: MutableList<String?> = mutableListOf()
|
||||
|
||||
init {
|
||||
initTabsDbEntities()
|
||||
}
|
||||
|
||||
private fun initTabsDbEntities() {
|
||||
val user = db.userDao().getActiveUser()!!
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
userId = user.user_id,
|
||||
instanceUri = user.instance_uri,
|
||||
)
|
||||
}
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
tabsDbEntities = db.tabsDao().getTabsChecked(_uiState.value.userId, _uiState.value.instanceUri)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun initTabsChecked(): Int {
|
||||
if (oldTabsChecked.isEmpty()) {
|
||||
// Only load tabsChecked if the model has not been updated
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
tabsChecked = if (_uiState.value.tabsDbEntities.isEmpty()) {
|
||||
// Load default menu
|
||||
Tab.defaultTabs.zip(List(Tab.defaultTabs.size){true}) + Tab.otherTabs.zip(List(Tab.otherTabs.size){false})
|
||||
} else {
|
||||
// Get current menu visibility and order from settings
|
||||
loadDbMenuTabs(_uiState.value.tabsDbEntities).toList()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
return _uiState.value.tabsChecked.size
|
||||
}
|
||||
|
||||
fun initTabsButtons(itemCount: Int, ctx: Context) {
|
||||
oldTabsChecked = _uiState.value.tabsChecked.toMutableList()
|
||||
if (oldTabsButtons.isEmpty()) {
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
tabsButtons = (0 until itemCount).map { null }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun tabsCheckReplace(position: Int, pair: Pair<Tab, Boolean>) {
|
||||
oldTabsChecked = _uiState.value.tabsChecked.toMutableList()
|
||||
oldTabsChecked[position] = pair
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
tabsChecked = oldTabsChecked.toList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun tabsCheckedRemove(position: Int): Pair<Tab, Boolean> {
|
||||
oldTabsChecked = _uiState.value.tabsChecked.toMutableList()
|
||||
val removedPair = oldTabsChecked.removeAt(position)
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
tabsChecked = oldTabsChecked.toList()
|
||||
)
|
||||
}
|
||||
return removedPair
|
||||
}
|
||||
|
||||
fun tabsCheckedAdd(position: Int, pair: Pair<Tab, Boolean>) {
|
||||
oldTabsChecked = _uiState.value.tabsChecked.toMutableList()
|
||||
oldTabsChecked.add(position, pair)
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
tabsChecked = oldTabsChecked.toList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTabsButtons(position: Int, text: String) {
|
||||
oldTabsButtons = _uiState.value.tabsButtons.toMutableList()
|
||||
oldTabsButtons[position] = text
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
tabsButtons = oldTabsButtons.toList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getTabsButtons(position: Int): String? {
|
||||
return _uiState.value.tabsButtons[position]
|
||||
}
|
||||
|
||||
fun swapTabsButtons(from: Int, to: Int) {
|
||||
oldTabsButtons = _uiState.value.tabsButtons.toMutableList()
|
||||
val fromText = oldTabsButtons[from]
|
||||
oldTabsButtons[from] = oldTabsButtons[to]
|
||||
oldTabsButtons[to] = fromText
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
tabsButtons = oldTabsButtons.toList()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class ArrangeTabsUiState(
|
||||
val userId: String = "",
|
||||
val instanceUri: String = "",
|
||||
val tabsDbEntities: List<TabsDatabaseEntity> = listOf(),
|
||||
val tabsChecked: List<Pair<Tab, Boolean>> = listOf(),
|
||||
val tabsButtons: List<String?> = listOf()
|
||||
)
|
@ -1,56 +0,0 @@
|
||||
package org.pixeldroid.app.settings
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.res.XmlResourceParser
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.pixeldroid.app.R
|
||||
|
||||
class LanguageSettingFragment : DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val list: MutableList<String> = mutableListOf()
|
||||
// IDE doesn't find it, but compiling works apparently?
|
||||
resources.getXml(R.xml._generated_res_locale_config).use {
|
||||
var eventType = it.eventType
|
||||
while (eventType != XmlResourceParser.END_DOCUMENT) {
|
||||
when (eventType) {
|
||||
XmlResourceParser.START_TAG -> {
|
||||
if (it.name == "locale") {
|
||||
list.add(it.getAttributeValue(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
eventType = it.next()
|
||||
}
|
||||
}
|
||||
val locales = AppCompatDelegate.getApplicationLocales()
|
||||
val checkedItem: Int =
|
||||
if(locales.isEmpty) 0
|
||||
else {
|
||||
// For some reason we get a bit inconsistent language tags. This normalises it for
|
||||
// the currently used languages, but it might break in the future if we add some
|
||||
val index = list.indexOf(locales.get(0)?.toLanguageTag()?.lowercase()?.replace('_', '-'))
|
||||
// If found, we want to compensate for the first in the list being the default
|
||||
if(index == -1) -1
|
||||
else index + 1
|
||||
}
|
||||
|
||||
return MaterialAlertDialogBuilder(requireContext()).apply {
|
||||
setIcon(R.drawable.translate_black_24dp)
|
||||
setTitle(R.string.language)
|
||||
setSingleChoiceItems((mutableListOf(getString(R.string.default_system)) + list.map {
|
||||
val appLocale = LocaleListCompat.forLanguageTags(it)
|
||||
appLocale.get(0)!!.getDisplayName(appLocale.get(0)!!)
|
||||
}).toTypedArray(), checkedItem) { dialog, which ->
|
||||
val languageTag = if(which in 1..list.size) list[which - 1] else null
|
||||
dialog.dismiss()
|
||||
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(languageTag))
|
||||
}
|
||||
setNegativeButton(android.R.string.ok) { _, _ -> }
|
||||
}.create()
|
||||
}
|
||||
}
|
@ -1,78 +1,48 @@
|
||||
package org.pixeldroid.app.settings
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.XmlResourceParser
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.activity.addCallback
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.CheckBoxPreference
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.getkeepsafe.taptargetview.TapTarget
|
||||
import com.getkeepsafe.taptargetview.TapTargetView
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.pixeldroid.app.MainActivity
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.SettingsBinding
|
||||
import org.pixeldroid.app.main.MainActivity
|
||||
import org.pixeldroid.app.settings.TutorialSettingsDialog.Companion.START_TUTORIAL
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.api.PixelfedAPI
|
||||
import org.pixeldroid.app.utils.api.objects.CommonWrapper
|
||||
import org.pixeldroid.common.ThemedActivity
|
||||
import org.pixeldroid.app.utils.setThemeFromPreferences
|
||||
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SettingsActivity : BaseActivity(), SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
class SettingsActivity : ThemedActivity(), SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private var restartMainOnExit = false
|
||||
private lateinit var binding: SettingsBinding
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = SettingsBinding.inflate(layoutInflater)
|
||||
val binding = SettingsBinding.inflate(layoutInflater)
|
||||
|
||||
setContentView(binding.root)
|
||||
setSupportActionBar(binding.topBar)
|
||||
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, SettingsFragment(), "topsettingsfragment")
|
||||
.replace(R.id.settings, SettingsFragment())
|
||||
.commit()
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
val showTutorial = intent.getBooleanExtra(START_TUTORIAL, false)
|
||||
|
||||
if(showTutorial){
|
||||
lifecycleScope.launch {
|
||||
var target =
|
||||
(supportFragmentManager.findFragmentByTag("topsettingsfragment") as? SettingsFragment)?.scrollToArrangeTabs(10)
|
||||
while (target == null) {
|
||||
target = (supportFragmentManager.findFragmentByTag("topsettingsfragment") as? SettingsFragment)?.scrollToArrangeTabs(10)
|
||||
delay(100)
|
||||
}
|
||||
target.performClick()
|
||||
}
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback(this /* lifecycle owner */) {
|
||||
// Handle the back button event
|
||||
// If a setting (for example language or theme) was changed, the main activity should be
|
||||
// started without history so that the change is applied to the whole back stack
|
||||
//TODO restore behaviour without true here, so that MainActivity is not destroyed when not necessary
|
||||
// The true is a "temporary" (lol) fix so that tab changes are always taken into account
|
||||
// Also, consider making the up button (arrow in action bar) also take this codepath!
|
||||
// It recreates the activity by default
|
||||
if (true || restartMainOnExit) {
|
||||
if (restartMainOnExit) {
|
||||
val intent = Intent(this@SettingsActivity, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
super@SettingsActivity.startActivity(intent)
|
||||
@ -109,27 +79,6 @@ class SettingsActivity : BaseActivity(), SharedPreferences.OnSharedPreferenceCha
|
||||
"themeColor" -> {
|
||||
recreateWithRestartStatus()
|
||||
}
|
||||
|
||||
"always_show_nsfw" -> {
|
||||
lifecycleScope.launch {
|
||||
val api: PixelfedAPI = apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
try {
|
||||
// Get old settings and modify just the nsfw one
|
||||
val settings = api.getSettings().let { settings ->
|
||||
settings.common.copy(
|
||||
media = settings.common.media.copy(
|
||||
always_show_cw = sharedPreferences.getBoolean(key, settings.common.media.always_show_cw)
|
||||
)
|
||||
)
|
||||
}
|
||||
api.setSettings(CommonWrapper(settings))
|
||||
} catch (e: Exception) {
|
||||
Log.e("Pixelfed API settings", e.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -147,36 +96,6 @@ class SettingsActivity : BaseActivity(), SharedPreferences.OnSharedPreferenceCha
|
||||
super.startActivity(intent)
|
||||
}
|
||||
|
||||
fun customTabsTutorial(){
|
||||
lifecycleScope.launch {
|
||||
var target =
|
||||
(supportFragmentManager.findFragmentByTag("topsettingsfragment") as? SettingsFragment)?.scrollToArrangeTabs(5)
|
||||
while (target == null) {
|
||||
target = (supportFragmentManager.findFragmentByTag("topsettingsfragment") as? SettingsFragment)?.scrollToArrangeTabs(5)
|
||||
delay(100)
|
||||
}
|
||||
TapTargetView.showFor(
|
||||
this@SettingsActivity,
|
||||
TapTarget.forView(target, getString(R.string.arrange_tabs_tutorial_title))
|
||||
.transparentTarget(true)
|
||||
.targetRadius(60), // Specify the target radius (in dp)
|
||||
object : TapTargetView.Listener() {
|
||||
// The listener can listen for regular clicks, long clicks or cancels
|
||||
override fun onTargetClick(view: TapTargetView?) {
|
||||
super.onTargetClick(view) // This call is optional
|
||||
// Perform action for the current target
|
||||
val dialogFragment = ArrangeTabsFragment().apply { showTutorial = true }
|
||||
dialogFragment.setTargetFragment(
|
||||
(supportFragmentManager.findFragmentByTag("topsettingsfragment") as? SettingsFragment),
|
||||
0
|
||||
)
|
||||
dialogFragment.show(supportFragmentManager, "settings_fragment")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class SettingsFragment : PreferenceFragmentCompat() {
|
||||
override fun onDisplayPreferenceDialog(preference: Preference) {
|
||||
var dialogFragment: DialogFragment? = null
|
||||
@ -184,10 +103,6 @@ class SettingsActivity : BaseActivity(), SharedPreferences.OnSharedPreferenceCha
|
||||
dialogFragment = ColorPreferenceDialog((preference as ColorPreference?)!!)
|
||||
} else if(preference.key == "language"){
|
||||
dialogFragment = LanguageSettingFragment()
|
||||
} else if (preference.key == "arrange_tabs") {
|
||||
dialogFragment = ArrangeTabsFragment()
|
||||
} else if (preference.key == "tutorial") {
|
||||
dialogFragment = TutorialSettingsDialog()
|
||||
}
|
||||
if (dialogFragment != null) {
|
||||
dialogFragment.setTargetFragment(this, 0)
|
||||
@ -196,16 +111,6 @@ class SettingsActivity : BaseActivity(), SharedPreferences.OnSharedPreferenceCha
|
||||
super.onDisplayPreferenceDialog(preference)
|
||||
}
|
||||
}
|
||||
fun scrollToArrangeTabs(position: Int): View? {
|
||||
//Hardcoded positions because it's too annoying to find!
|
||||
|
||||
if (listView != null && position != -1) {
|
||||
listView.post {
|
||||
listView.smoothScrollToPosition(position)
|
||||
}
|
||||
}
|
||||
return listView.findViewHolderForAdapterPosition(position)?.itemView
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.root_preferences, rootKey)
|
||||
@ -216,16 +121,6 @@ class SettingsActivity : BaseActivity(), SharedPreferences.OnSharedPreferenceCha
|
||||
locale?.getDisplayName(locale) ?: getString(R.string.default_system)
|
||||
}
|
||||
}
|
||||
findPreference<CheckBoxPreference>("always_show_nsfw")?.let {
|
||||
lifecycleScope.launch {
|
||||
val api: PixelfedAPI = (requireActivity() as SettingsActivity).apiHolder.api ?: (requireActivity() as SettingsActivity).apiHolder.setToCurrentUser()
|
||||
|
||||
try {
|
||||
val show = api.getSettings().common.media.always_show_cw
|
||||
it.isChecked = show
|
||||
} catch (_: Exception){}
|
||||
}
|
||||
}
|
||||
|
||||
//Hide Notification setting for Android versions where it doesn't work
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
@ -234,4 +129,49 @@ class SettingsActivity : BaseActivity(), SharedPreferences.OnSharedPreferenceCha
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
class LanguageSettingFragment : DialogFragment() {
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val list: MutableList<String> = mutableListOf()
|
||||
// IDE doesn't find it, but compiling works apparently?
|
||||
resources.getXml(R.xml._generated_res_locale_config).use {
|
||||
var eventType = it.eventType
|
||||
while (eventType != XmlResourceParser.END_DOCUMENT) {
|
||||
when (eventType) {
|
||||
XmlResourceParser.START_TAG -> {
|
||||
if (it.name == "locale") {
|
||||
list.add(it.getAttributeValue(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
eventType = it.next()
|
||||
}
|
||||
}
|
||||
val locales = AppCompatDelegate.getApplicationLocales()
|
||||
val checkedItem: Int =
|
||||
if(locales.isEmpty) 0
|
||||
else {
|
||||
// For some reason we get a bit inconsistent language tags. This normalises it for
|
||||
// the currently used languages, but it might break in the future if we add some
|
||||
val index = list.indexOf(locales.get(0)?.toLanguageTag()?.lowercase()?.replace('_', '-'))
|
||||
// If found, we want to compensate for the first in the list being the default
|
||||
if(index == -1) -1
|
||||
else index + 1
|
||||
}
|
||||
|
||||
return MaterialAlertDialogBuilder(requireContext()).apply {
|
||||
setIcon(R.drawable.translate_black_24dp)
|
||||
setTitle(R.string.language)
|
||||
setSingleChoiceItems((mutableListOf(getString(R.string.default_system)) + list.map {
|
||||
val appLocale = LocaleListCompat.forLanguageTags(it)
|
||||
appLocale.get(0)!!.getDisplayName(appLocale.get(0)!!)
|
||||
}).toTypedArray(), checkedItem) { dialog, which ->
|
||||
val languageTag = if(which in 1..list.size) list[which - 1] else null
|
||||
dialog.dismiss()
|
||||
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(languageTag))
|
||||
}
|
||||
setNegativeButton(android.R.string.ok) { _, _ -> }
|
||||
}.create()
|
||||
}
|
||||
}
|
||||
|
@ -1,77 +0,0 @@
|
||||
package org.pixeldroid.app.settings;
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Intent
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.getkeepsafe.taptargetview.TapTarget
|
||||
import com.getkeepsafe.taptargetview.TapTargetView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.main.MainActivity
|
||||
import org.pixeldroid.app.utils.Tab
|
||||
|
||||
class TutorialSettingsDialog: DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val items = arrayOf(
|
||||
Pair(R.string.feeds_tutorial, R.drawable.ic_home_white_24dp),
|
||||
Pair(R.string.create_tutorial, R.drawable.photo_camera),
|
||||
Pair(R.string.dm_tutorial, R.drawable.message),
|
||||
Pair(R.string.custom_tabs_tutorial, R.drawable.outline_bottom_navigation)
|
||||
)
|
||||
|
||||
val adapter = object : ArrayAdapter<Pair<Int, Int>>(requireContext(), android.R.layout.simple_list_item_1, items) {
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
|
||||
val view: TextView = if (convertView == null) {
|
||||
LayoutInflater.from(context).inflate(android.R.layout.simple_list_item_1, parent, false) as TextView
|
||||
} else {
|
||||
convertView as TextView
|
||||
}
|
||||
|
||||
val item = getItem(position)
|
||||
|
||||
if (item != null) {
|
||||
view.setText(item.first)
|
||||
view.setTypeface(null, Typeface.NORMAL) // Set the typeface to normal
|
||||
view.setCompoundDrawablesWithIntrinsicBounds(item.second, 0, 0, 0)
|
||||
view.compoundDrawablePadding = 16 // Add padding between text and drawable
|
||||
}
|
||||
|
||||
view.setPadding(0, 32, 0, 32)
|
||||
return view
|
||||
}
|
||||
}
|
||||
return MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(getString(R.string.tutorial_choice))
|
||||
.setAdapter(adapter) { _, which ->
|
||||
if(which == 3){
|
||||
customTabsTutorial()
|
||||
return@setAdapter
|
||||
}
|
||||
val intent = Intent(requireContext(), MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
intent.putExtra(START_TUTORIAL, which)
|
||||
startActivity(intent)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.create()
|
||||
}
|
||||
|
||||
private fun customTabsTutorial() {
|
||||
(requireActivity() as SettingsActivity).customTabsTutorial()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val START_TUTORIAL = "tutorial_start_intent"
|
||||
}
|
||||
}
|
@ -25,6 +25,9 @@ import org.pixeldroid.app.databinding.ActivityStoriesBinding
|
||||
import org.pixeldroid.app.posts.setTextViewFromISO8601
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.api.objects.Account
|
||||
import org.pixeldroid.app.utils.api.objects.Story
|
||||
import org.pixeldroid.app.utils.api.objects.StoryCarousel
|
||||
|
||||
|
||||
class StoriesActivity: BaseActivity() {
|
||||
|
||||
@ -34,11 +37,12 @@ class StoriesActivity: BaseActivity() {
|
||||
const val STORY_CAROUSEL_USER_ID = "LaunchStoryUserId"
|
||||
}
|
||||
|
||||
|
||||
private lateinit var binding: ActivityStoriesBinding
|
||||
|
||||
private lateinit var storyProgress: StoryProgress
|
||||
|
||||
private val model: StoriesViewModel by viewModels()
|
||||
private lateinit var model: StoriesViewModel
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
//force night mode always
|
||||
@ -46,9 +50,18 @@ class StoriesActivity: BaseActivity() {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val carousel = intent.getSerializableExtra(STORY_CAROUSEL) as? StoryCarousel
|
||||
val userId = intent.getStringExtra(STORY_CAROUSEL_USER_ID)
|
||||
val selfCarousel: Array<Story>? = intent.getSerializableExtra(STORY_CAROUSEL_SELF) as? Array<Story>
|
||||
|
||||
binding = ActivityStoriesBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
val _model: StoriesViewModel by viewModels {
|
||||
StoriesViewModelFactory(application, carousel, userId, selfCarousel?.asList())
|
||||
}
|
||||
model = _model
|
||||
|
||||
storyProgress = StoryProgress(model.uiState.value.imageList.size)
|
||||
binding.storyProgressImage.setImageDrawable(storyProgress)
|
||||
|
||||
|
@ -1,18 +1,20 @@
|
||||
package org.pixeldroid.app.stories
|
||||
|
||||
import android.app.Application
|
||||
import android.os.CountDownTimer
|
||||
import android.text.Editable
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.utils.PixelDroidApplication
|
||||
import org.pixeldroid.app.utils.api.objects.CarouselUserContainer
|
||||
import org.pixeldroid.app.utils.api.objects.Story
|
||||
import org.pixeldroid.app.utils.api.objects.StoryCarousel
|
||||
@ -35,13 +37,18 @@ data class StoriesUiState(
|
||||
val snackBar: Int? = null,
|
||||
val reply: String = ""
|
||||
)
|
||||
@HiltViewModel
|
||||
class StoriesViewModel @Inject constructor(state: SavedStateHandle,
|
||||
db: AppDatabase,
|
||||
private val apiHolder: PixelfedAPIHolder) : ViewModel() {
|
||||
private val carousel: StoryCarousel? = state[StoriesActivity.STORY_CAROUSEL]
|
||||
private val userId: String? = state[StoriesActivity.STORY_CAROUSEL_USER_ID]
|
||||
private val selfCarousel: Array<Story>? = state[StoriesActivity.STORY_CAROUSEL_SELF]
|
||||
|
||||
class StoriesViewModel(
|
||||
application: Application,
|
||||
val carousel: StoryCarousel?,
|
||||
userId: String?,
|
||||
val selfCarousel: List<Story>?
|
||||
) : AndroidViewModel(application) {
|
||||
|
||||
@Inject
|
||||
lateinit var apiHolder: PixelfedAPIHolder
|
||||
@Inject
|
||||
lateinit var db: AppDatabase
|
||||
|
||||
private var currentAccount: CarouselUserContainer?
|
||||
|
||||
@ -54,9 +61,10 @@ class StoriesViewModel @Inject constructor(state: SavedStateHandle,
|
||||
private var timer: CountDownTimer? = null
|
||||
|
||||
init {
|
||||
(application as PixelDroidApplication).getAppComponent().inject(this)
|
||||
currentAccount =
|
||||
if (selfCarousel != null) {
|
||||
db.userDao().getActiveUser()?.let { CarouselUserContainer(it, selfCarousel.toList()) }
|
||||
db.userDao().getActiveUser()?.let { CarouselUserContainer(it, selfCarousel) }
|
||||
} else carousel?.nodes?.firstOrNull { it?.user?.id == userId }
|
||||
|
||||
_uiState = MutableStateFlow(newUiStateFromCurrentAccount())
|
||||
@ -208,3 +216,14 @@ class StoriesViewModel @Inject constructor(state: SavedStateHandle,
|
||||
fun currentProfileId(): String? = currentAccount?.user?.id
|
||||
|
||||
}
|
||||
|
||||
class StoriesViewModelFactory(
|
||||
val application: Application,
|
||||
val carousel: StoryCarousel?,
|
||||
val userId: String?,
|
||||
val selfCarousel: List<Story>?
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.getConstructor(Application::class.java, StoryCarousel::class.java, String::class.java, List::class.java).newInstance(application, carousel, userId, selfCarousel)
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,10 @@
|
||||
package org.pixeldroid.app.utils
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import android.os.Bundle
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
open class BaseActivity : org.pixeldroid.common.ThemedActivity() {
|
||||
|
||||
@Inject
|
||||
@ -13,6 +12,11 @@ open class BaseActivity : org.pixeldroid.common.ThemedActivity() {
|
||||
@Inject
|
||||
lateinit var apiHolder: PixelfedAPIHolder
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
(this.application as PixelDroidApplication).getAppComponent().inject(this)
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
return true
|
||||
|
@ -1,9 +1,9 @@
|
||||
package org.pixeldroid.app.utils
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
@ -12,7 +12,6 @@ import javax.inject.Inject
|
||||
/**
|
||||
* Base Fragment, for dependency injection and other things common to a lot of the fragments
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
open class BaseFragment: Fragment() {
|
||||
|
||||
@Inject
|
||||
@ -21,6 +20,11 @@ open class BaseFragment: Fragment() {
|
||||
@Inject
|
||||
lateinit var db: AppDatabase
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
(requireActivity().application as PixelDroidApplication).getAppComponent().inject(this)
|
||||
}
|
||||
|
||||
internal val requestPermissionDownloadPic =
|
||||
registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
|
@ -1,29 +1,15 @@
|
||||
package org.pixeldroid.app.utils
|
||||
|
||||
import android.app.Application
|
||||
import androidx.hilt.work.HiltWorkerFactory
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.work.Configuration
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.EntryPoints
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.ligi.tracedroid.TraceDroid
|
||||
import javax.inject.Inject
|
||||
import org.pixeldroid.app.utils.di.*
|
||||
|
||||
@HiltAndroidApp
|
||||
class PixelDroidApplication : Application(), Configuration.Provider {
|
||||
|
||||
@EntryPoint
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface HiltWorkerFactoryEntryPoint {
|
||||
fun workerFactory(): HiltWorkerFactory
|
||||
}
|
||||
override val workManagerConfiguration =
|
||||
Configuration.Builder()
|
||||
.setWorkerFactory(EntryPoints.get(this, HiltWorkerFactoryEntryPoint::class.java).workerFactory()) .build()
|
||||
class PixelDroidApplication: Application() {
|
||||
|
||||
private lateinit var mApplicationComponent: ApplicationComponent
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
@ -33,7 +19,18 @@ class PixelDroidApplication : Application(), Configuration.Provider {
|
||||
val sharedPreferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(this)
|
||||
setThemeFromPreferences(sharedPreferences, resources)
|
||||
mApplicationComponent = DaggerApplicationComponent
|
||||
.builder()
|
||||
.applicationModule(ApplicationModule(this))
|
||||
.databaseModule(DatabaseModule(applicationContext))
|
||||
.aPIModule(APIModule())
|
||||
.build()
|
||||
mApplicationComponent.inject(this)
|
||||
|
||||
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||
}
|
||||
|
||||
fun getAppComponent(): ApplicationComponent {
|
||||
return mApplicationComponent
|
||||
}
|
||||
}
|
@ -1,27 +1,28 @@
|
||||
package org.pixeldroid.app.utils
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.*
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.ImageDecoder
|
||||
import android.graphics.Matrix
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.MediaStore
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.WindowManager
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.color.MaterialColors
|
||||
@ -31,11 +32,9 @@ import com.google.gson.JsonPrimitive
|
||||
import com.google.gson.JsonSerializer
|
||||
import okhttp3.HttpUrl
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.db.entities.TabsDatabaseEntity
|
||||
import java.time.Instant
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
import java.util.*
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
@ -148,8 +147,8 @@ fun setThemeFromPreferences(preferences: SharedPreferences, resources: Resources
|
||||
themes[1] -> {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||
}
|
||||
//Dark or AMOLED dark
|
||||
themes[2], themes[3] -> {
|
||||
//Dark
|
||||
themes[2] -> {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
||||
}
|
||||
else -> {
|
||||
@ -196,88 +195,4 @@ fun <T> Fragment.bindingLifecycleAware(): ReadWriteProperty<Fragment, T> =
|
||||
binding = value
|
||||
this@bindingLifecycleAware.viewLifecycleOwner.lifecycle.addObserver(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadDbMenuTabs(tabsDbEntry: List<TabsDatabaseEntity>): List<Pair<Tab, Boolean>> {
|
||||
return tabsDbEntry.map {
|
||||
val tab = Tab.fromName(it.tab)
|
||||
if (tab == Tab.HASHTAG_FEED) {
|
||||
tab.filter = it.filter
|
||||
}
|
||||
Pair(tab, it.checked)
|
||||
}
|
||||
}
|
||||
|
||||
enum class Tab(var filter: String? = null) {
|
||||
HOME_FEED, SEARCH_DISCOVER_FEED, CREATE_FEED, NOTIFICATIONS_FEED, PUBLIC_FEED, DIRECT_MESSAGES, HASHTAG_FEED;
|
||||
|
||||
fun toLanguageString(ctx: Context, db: AppDatabase, index: Int, lookForFilter: Boolean = false): String {
|
||||
return when (this) {
|
||||
HOME_FEED -> ctx.getString(R.string.home_feed)
|
||||
SEARCH_DISCOVER_FEED -> ctx.getString(R.string.search_discover_feed)
|
||||
CREATE_FEED -> ctx.getString(R.string.create_feed)
|
||||
NOTIFICATIONS_FEED -> ctx.getString(R.string.notifications_feed)
|
||||
PUBLIC_FEED -> ctx.getString(R.string.public_feed)
|
||||
DIRECT_MESSAGES -> ctx.getString(R.string.direct_messages)
|
||||
HASHTAG_FEED -> {
|
||||
val user = db.userDao().getActiveUser()!!
|
||||
val hashtag = db.tabsDao().getTabChecked(index, user.user_id, user.instance_uri)?.filter
|
||||
if (lookForFilter && hashtag != null) {
|
||||
StringBuilder("#").append(hashtag).toString()
|
||||
} else {
|
||||
ctx.getString(R.string.feed_hashtag)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toName(): String {
|
||||
return this.name
|
||||
}
|
||||
|
||||
fun getDrawable(ctx: Context): Drawable? {
|
||||
val resId = when (this) {
|
||||
HOME_FEED -> R.drawable.selector_home_feed
|
||||
SEARCH_DISCOVER_FEED -> R.drawable.ic_search_white_24dp
|
||||
CREATE_FEED -> R.drawable.selector_camera
|
||||
NOTIFICATIONS_FEED -> R.drawable.selector_notifications
|
||||
PUBLIC_FEED -> R.drawable.ic_filter_black_24dp
|
||||
DIRECT_MESSAGES -> R.drawable.selector_dm
|
||||
HASHTAG_FEED -> R.drawable.feed_hashtag
|
||||
}
|
||||
return AppCompatResources.getDrawable(ctx, resId)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromLanguageString(ctx: Context, name: String): Tab {
|
||||
return when (name) {
|
||||
ctx.getString(R.string.home_feed) -> HOME_FEED
|
||||
ctx.getString(R.string.search_discover_feed) -> SEARCH_DISCOVER_FEED
|
||||
ctx.getString(R.string.create_feed) -> CREATE_FEED
|
||||
ctx.getString(R.string.notifications_feed) -> NOTIFICATIONS_FEED
|
||||
ctx.getString(R.string.public_feed) -> PUBLIC_FEED
|
||||
ctx.getString(R.string.direct_messages) -> DIRECT_MESSAGES
|
||||
ctx.getString(R.string.feed_hashtag) -> HASHTAG_FEED
|
||||
else -> HOME_FEED
|
||||
}
|
||||
}
|
||||
|
||||
fun fromName(name: String): Tab {
|
||||
return entries.filter { it.name == name }.getOrElse(0) { HOME_FEED }
|
||||
}
|
||||
|
||||
val defaultTabs: List<Tab>
|
||||
get() = listOf(
|
||||
HOME_FEED,
|
||||
SEARCH_DISCOVER_FEED,
|
||||
CREATE_FEED,
|
||||
NOTIFICATIONS_FEED,
|
||||
PUBLIC_FEED
|
||||
)
|
||||
val otherTabs: List<Tab>
|
||||
get() = listOf(
|
||||
DIRECT_MESSAGES,
|
||||
HASHTAG_FEED
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -138,12 +138,6 @@ interface PixelfedAPI {
|
||||
@Field("reblogs") reblogs : Boolean = true
|
||||
) : Relationship
|
||||
|
||||
@POST("/api/v1.1/direct/lookup")
|
||||
suspend fun lookupUser(
|
||||
@Query("q") q: String,
|
||||
@Query("remote") remote : Boolean = false
|
||||
) : List<LookupUser>
|
||||
|
||||
@POST("/api/v1/accounts/{id}/unfollow")
|
||||
suspend fun unfollow(
|
||||
@Path("id") statusId: String,
|
||||
@ -338,37 +332,42 @@ interface PixelfedAPI {
|
||||
@Query("account_id") account_id: Boolean? = null
|
||||
): List<Notification>
|
||||
|
||||
@GET("/api/v1/conversations")
|
||||
suspend fun viewAllConversations(
|
||||
@Query("max_id") max_id: String? = null,
|
||||
@Query("since_id") since_id: String? = null,
|
||||
@Query("min_id") min_id: String? = null,
|
||||
@Query("limit") limit: String? = null
|
||||
): List<Conversation>
|
||||
|
||||
@DELETE("/api/v1/conversations/{id}")
|
||||
suspend fun deleteConversation(
|
||||
@Path("id") id: String
|
||||
)
|
||||
|
||||
@POST("/api/v1/conversations/{id}/read")
|
||||
suspend fun markConversationAsRead(
|
||||
@Path("id") id: String
|
||||
): Conversation
|
||||
|
||||
@GET("/api/v1/accounts/verify_credentials")
|
||||
suspend fun verifyCredentials(
|
||||
//The authorization header needs to be of the form "Bearer <token>"
|
||||
@Header("Authorization") authorization: String? = null
|
||||
): Account
|
||||
|
||||
//@Multipart
|
||||
@PATCH("/api/v1/accounts/update_credentials")
|
||||
suspend fun updateCredentials(
|
||||
@Query(value = "display_name") displayName: String?,
|
||||
@Query(value = "note") note: String?,
|
||||
@Query(value = "locked") locked: Boolean?,
|
||||
// @Part avatar: MultipartBody.Part?,
|
||||
): Account
|
||||
|
||||
/**
|
||||
* Pixelfed uses PHP, multipart uploads don't work through PATCH so we use POST as suggested
|
||||
* here: https://github.com/pixelfed/pixelfed/issues/4250
|
||||
* However, changing to POST breaks the upload on Mastodon.
|
||||
*
|
||||
* To have this work on Pixelfed and Mastodon without special logic to distinguish the two,
|
||||
* we'll have to wait for PHP 8.4 and https://wiki.php.net/rfc/rfc1867-non-post
|
||||
* which should come out end of 2024
|
||||
*/
|
||||
@Multipart
|
||||
@POST("/api/v1/accounts/update_credentials")
|
||||
fun updateProfilePicture(
|
||||
@Part avatar: MultipartBody.Part?
|
||||
): Observable<Account>
|
||||
|
||||
@Multipart
|
||||
@PATCH("/api/v1/accounts/update_credentials")
|
||||
fun updateProfilePictureMastodon(
|
||||
fun updateProfilePicture(
|
||||
@Part avatar: MultipartBody.Part?
|
||||
): Observable<Account>
|
||||
|
||||
@ -435,30 +434,6 @@ interface PixelfedAPI {
|
||||
@GET("/api/v1.1/discover/posts/hashtags")
|
||||
suspend fun trendingHashtags() : List<Tag>
|
||||
|
||||
@GET("/api/v1/conversations")
|
||||
suspend fun directMessagesList(
|
||||
// @Query("max_id") max_id: String? = null,
|
||||
// @Query("since_id") since_id: String? = null,
|
||||
// @Query("min_id") min_id: String? = null,
|
||||
@Query("page") page: Int? = null,
|
||||
@Query("limit") limit: String? = null,
|
||||
): List<Conversation>
|
||||
|
||||
@GET("/api/v1.1/direct/thread")
|
||||
suspend fun directMessagesConversation(
|
||||
@Query("pid") pid: String? = null,
|
||||
@Query("max_id") max_id: String? = null,
|
||||
@Query("min_id") min_id: String? = null,
|
||||
): DMThread
|
||||
|
||||
@POST("/api/v1.1/direct/thread/send")
|
||||
suspend fun sendDirectMessage(
|
||||
@Query("to_id") to_id: String? = null,
|
||||
@Query("message") message: String? = null,
|
||||
// text or emoji
|
||||
@Query("type") min_id: String = "text",
|
||||
): DMThread
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("/api/v1/reports")
|
||||
@JvmSuppressWildcards
|
||||
@ -469,11 +444,4 @@ interface PixelfedAPI {
|
||||
@Field("forward") forward: Boolean = true
|
||||
) : Report
|
||||
|
||||
@POST("/api/pixelfed/v1/app/settings")
|
||||
suspend fun setSettings(
|
||||
@Body account_id: CommonWrapper,
|
||||
) : PixelfedAppSettings
|
||||
|
||||
@GET("/api/pixelfed/v1/app/settings")
|
||||
suspend fun getSettings() : PixelfedAppSettings
|
||||
}
|
||||
|
@ -10,8 +10,9 @@ import java.io.Serializable
|
||||
Represents a conversation.
|
||||
https://docs.joinmastodon.org/entities/Conversation/
|
||||
*/
|
||||
|
||||
@Entity(
|
||||
tableName = "directMessages",
|
||||
tableName = "direct_messages",
|
||||
primaryKeys = ["id", "user_id", "instance_uri"],
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = UserDatabaseEntity::class,
|
||||
@ -23,13 +24,18 @@ https://docs.joinmastodon.org/entities/Conversation/
|
||||
indices = [Index(value = ["user_id", "instance_uri"])]
|
||||
)
|
||||
data class Conversation(
|
||||
override val id: String,
|
||||
val unread: Boolean?,
|
||||
val accounts: List<Account>?,
|
||||
val last_status: Status?,
|
||||
//Base attributes
|
||||
override val id: String?,
|
||||
val unread: Boolean? = true,
|
||||
val accounts: List<Account>? = null,
|
||||
val last_status: Status? = null,
|
||||
|
||||
//Database values (not from API)
|
||||
//TODO do we find this approach acceptable? Preferable to a semi-duplicate ConversationDataBaseEntity?
|
||||
//TODO do we find this approach acceptable? Preferable to a semi-duplicate NotificationDataBaseEntity?
|
||||
override var user_id: String,
|
||||
override var instance_uri: String,
|
||||
): FeedContent, FeedContentDatabase, Serializable
|
||||
): FeedContent, FeedContentDatabase, Serializable {
|
||||
enum class NotificationType : Serializable {
|
||||
follow, follow_request, mention, reblog, favourite, poll, status, comment //comment is Pixelfed-specific?
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
package org.pixeldroid.app.utils.api.objects
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import java.io.Serializable
|
||||
import java.time.Instant
|
||||
|
||||
|
||||
/*
|
||||
Represents a conversation.
|
||||
https://docs.joinmastodon.org/entities/Conversation/
|
||||
*/
|
||||
data class DMThread(
|
||||
override val id: String,
|
||||
val name: String?,
|
||||
val username: String?,
|
||||
val avatar: String?,
|
||||
val url: String?,
|
||||
val muted: Boolean?,
|
||||
val isLocal: Boolean?,
|
||||
val domain: String?,
|
||||
val created_at: Instant?, //ISO 8601 Datetime
|
||||
val updated_at: Instant?,
|
||||
val timeAgo: String?,
|
||||
val lastMessage: String?,
|
||||
val messages: List<Message>,
|
||||
): FeedContent, Serializable
|
@ -1,6 +0,0 @@
|
||||
package org.pixeldroid.app.utils.api.objects
|
||||
|
||||
data class LookupUser(
|
||||
val name: String,
|
||||
val id: String
|
||||
)
|
@ -1,19 +0,0 @@
|
||||
package org.pixeldroid.app.utils.api.objects
|
||||
|
||||
import java.io.Serializable
|
||||
import java.time.Instant
|
||||
|
||||
data class Message(
|
||||
override val id: String,
|
||||
val name: String?,
|
||||
val hidden: Boolean?,
|
||||
val isAuthor: Boolean?,
|
||||
val type: String?, //TODO enum?
|
||||
val text: String?,
|
||||
val media: String?, //TODO,
|
||||
val carousel: List<Attachment>?,
|
||||
val created_at: Instant?, //ISO 8601 Datetime
|
||||
val timeAgo: String?,
|
||||
val reportId: String?,
|
||||
//val meta: String?, //TODO
|
||||
): FeedContent, Serializable
|
@ -1,34 +0,0 @@
|
||||
package org.pixeldroid.app.utils.api.objects
|
||||
|
||||
data class PixelfedAppSettings(
|
||||
val id: String,
|
||||
val username: String,
|
||||
val updated_at: String,
|
||||
val common: Common
|
||||
)
|
||||
data class CommonWrapper(
|
||||
val common: Common
|
||||
)
|
||||
|
||||
data class Common(
|
||||
val timelines: Timelines,
|
||||
val media: Media,
|
||||
val appearance: Appearance
|
||||
)
|
||||
|
||||
data class Timelines(
|
||||
val show_public: Boolean,
|
||||
val show_network: Boolean,
|
||||
val hide_likes_shares: Boolean
|
||||
)
|
||||
|
||||
data class Media(
|
||||
val hide_public_behind_cw: Boolean,
|
||||
val always_show_cw: Boolean,
|
||||
val show_alt_text: Boolean
|
||||
)
|
||||
|
||||
data class Appearance(
|
||||
val links_use_in_app_browser: Boolean,
|
||||
val theme: String
|
||||
)
|
@ -1,27 +1,21 @@
|
||||
package org.pixeldroid.app.utils.db
|
||||
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import org.pixeldroid.app.utils.api.objects.Conversation
|
||||
import org.pixeldroid.app.utils.api.objects.Notification
|
||||
import org.pixeldroid.app.utils.db.dao.InstanceDao
|
||||
import org.pixeldroid.app.utils.db.dao.TabsDao
|
||||
import org.pixeldroid.app.utils.db.dao.UserDao
|
||||
import org.pixeldroid.app.utils.db.dao.feedContent.DirectMessagesConversationDao
|
||||
import org.pixeldroid.app.utils.db.dao.feedContent.DirectMessagesDao
|
||||
import org.pixeldroid.app.utils.db.dao.*
|
||||
import org.pixeldroid.app.utils.db.dao.feedContent.NotificationDao
|
||||
import org.pixeldroid.app.utils.db.dao.feedContent.posts.HomePostDao
|
||||
import org.pixeldroid.app.utils.db.dao.feedContent.posts.PublicPostDao
|
||||
import org.pixeldroid.app.utils.db.entities.DirectMessageDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.entities.HomeStatusDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.entities.PublicFeedStatusDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.entities.TabsDatabaseEntity
|
||||
import org.pixeldroid.app.utils.api.objects.Notification
|
||||
import org.pixeldroid.app.utils.api.objects.Conversation
|
||||
import org.pixeldroid.app.utils.db.dao.feedContent.DirectMessagesDao
|
||||
|
||||
@Database(entities = [
|
||||
InstanceDatabaseEntity::class,
|
||||
@ -29,18 +23,10 @@ import org.pixeldroid.app.utils.db.entities.TabsDatabaseEntity
|
||||
HomeStatusDatabaseEntity::class,
|
||||
PublicFeedStatusDatabaseEntity::class,
|
||||
Notification::class,
|
||||
TabsDatabaseEntity::class,
|
||||
Conversation::class,
|
||||
DirectMessageDatabaseEntity::class,
|
||||
Conversation::class
|
||||
],
|
||||
autoMigrations = [
|
||||
AutoMigration(from = 6, to = 7),
|
||||
AutoMigration(from = 7, to = 8),
|
||||
AutoMigration(from = 8, to = 9),
|
||||
],
|
||||
version = 10
|
||||
version = 5
|
||||
)
|
||||
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun instanceDao(): InstanceDao
|
||||
@ -48,9 +34,7 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun homePostDao(): HomePostDao
|
||||
abstract fun publicPostDao(): PublicPostDao
|
||||
abstract fun notificationDao(): NotificationDao
|
||||
abstract fun tabsDao(): TabsDao
|
||||
abstract fun directMessagesDao(): DirectMessagesDao
|
||||
abstract fun directMessagesConversationDao(): DirectMessagesConversationDao
|
||||
}
|
||||
|
||||
val MIGRATION_3_4 = object : Migration(3, 4) {
|
||||
@ -64,16 +48,4 @@ val MIGRATION_4_5 = object : Migration(4, 5) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE instances ADD COLUMN videoEnabled INTEGER NOT NULL DEFAULT 1")
|
||||
}
|
||||
}
|
||||
val MIGRATION_5_6 = object : Migration(5, 6) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE instances ADD COLUMN pixelfed INTEGER NOT NULL DEFAULT 1")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATION_9_10 = object : Migration(9, 10) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
// Sorry users, this was just easier
|
||||
database.execSQL("DELETE FROM tabsChecked")
|
||||
}
|
||||
}
|
@ -137,15 +137,6 @@ class Converters {
|
||||
Status.Visibility::class.java
|
||||
)
|
||||
|
||||
@TypeConverter
|
||||
fun accountListToJson(type: List<Account>?): String {
|
||||
val listType = object : TypeToken<List<Account?>?>() {}.type
|
||||
return gson.toJson(type, listType)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToAccountList(json: String): List<Account>? {
|
||||
val listType = object : TypeToken<List<Account?>?>() {}.type
|
||||
return gson.fromJson(json, listType)
|
||||
}
|
||||
|
||||
}
|
@ -13,58 +13,41 @@ import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity.Companion.DEF
|
||||
import org.pixeldroid.app.utils.normalizeDomain
|
||||
import java.lang.IllegalArgumentException
|
||||
|
||||
suspend fun addUser(
|
||||
db: AppDatabase, account: Account, instance_uri: String, activeUser: Boolean = true,
|
||||
accessToken: String, refreshToken: String?, clientId: String, clientSecret: String,
|
||||
) {
|
||||
fun addUser(db: AppDatabase, account: Account, instance_uri: String, activeUser: Boolean = true,
|
||||
accessToken: String, refreshToken: String?, clientId: String, clientSecret: String) {
|
||||
db.userDao().insertOrUpdate(
|
||||
UserDatabaseEntity(
|
||||
user_id = account.id!!,
|
||||
instance_uri = normalizeDomain(instance_uri),
|
||||
username = account.username!!,
|
||||
display_name = account.getDisplayName(),
|
||||
avatar_static = account.anyAvatar().orEmpty(),
|
||||
isActive = activeUser,
|
||||
accessToken = accessToken,
|
||||
refreshToken = refreshToken,
|
||||
clientId = clientId,
|
||||
clientSecret = clientSecret
|
||||
)
|
||||
UserDatabaseEntity(
|
||||
user_id = account.id!!,
|
||||
instance_uri = normalizeDomain(instance_uri),
|
||||
username = account.username!!,
|
||||
display_name = account.getDisplayName(),
|
||||
avatar_static = account.anyAvatar().orEmpty(),
|
||||
isActive = activeUser,
|
||||
accessToken = accessToken,
|
||||
refreshToken = refreshToken,
|
||||
clientId = clientId,
|
||||
clientSecret = clientSecret
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun updateUserInfoDb(db: AppDatabase, account: Account) {
|
||||
val user = db.userDao().getActiveUser()!!
|
||||
db.userDao().updateUserAccountDetails(
|
||||
account.username.orEmpty(),
|
||||
account.display_name.orEmpty(),
|
||||
account.anyAvatar().orEmpty(),
|
||||
user.user_id,
|
||||
user.instance_uri
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun storeInstance(db: AppDatabase, nodeInfo: NodeInfo?, instance: Instance? = null) {
|
||||
fun storeInstance(db: AppDatabase, nodeInfo: NodeInfo?, instance: Instance? = null) {
|
||||
val dbInstance: InstanceDatabaseEntity = nodeInfo?.run {
|
||||
InstanceDatabaseEntity(
|
||||
uri = normalizeDomain(metadata?.config?.site?.url!!),
|
||||
title = metadata.config.site.name!!,
|
||||
maxStatusChars = metadata.config.uploader?.max_caption_length!!.toInt(),
|
||||
maxPhotoSize = metadata.config.uploader.max_photo_size?.toIntOrNull()
|
||||
?: DEFAULT_MAX_PHOTO_SIZE,
|
||||
maxPhotoSize = metadata.config.uploader.max_photo_size?.toIntOrNull() ?: DEFAULT_MAX_PHOTO_SIZE,
|
||||
// Pixelfed doesn't distinguish between max photo and video size
|
||||
maxVideoSize = metadata.config.uploader.max_photo_size?.toIntOrNull()
|
||||
?: DEFAULT_MAX_VIDEO_SIZE,
|
||||
maxVideoSize = metadata.config.uploader.max_photo_size?.toIntOrNull() ?: DEFAULT_MAX_VIDEO_SIZE,
|
||||
albumLimit = metadata.config.uploader.album_limit?.toIntOrNull() ?: DEFAULT_ALBUM_LIMIT,
|
||||
videoEnabled = metadata.config.features?.video ?: DEFAULT_VIDEO_ENABLED,
|
||||
pixelfed = metadata.software?.repo?.contains("pixelfed", ignoreCase = true) == true
|
||||
videoEnabled = metadata.config.features?.video ?: DEFAULT_VIDEO_ENABLED
|
||||
)
|
||||
} ?: instance?.run {
|
||||
InstanceDatabaseEntity(
|
||||
uri = normalizeDomain(uri.orEmpty()),
|
||||
title = title.orEmpty(),
|
||||
maxStatusChars = max_toot_chars?.toInt() ?: DEFAULT_MAX_TOOT_CHARS,
|
||||
pixelfed = false
|
||||
uri = normalizeDomain(uri.orEmpty()),
|
||||
title = title.orEmpty(),
|
||||
maxStatusChars = max_toot_chars?.toInt() ?: DEFAULT_MAX_TOOT_CHARS,
|
||||
)
|
||||
} ?: throw IllegalArgumentException("Cannot store instance where both are null")
|
||||
|
||||
|
@ -1,33 +1,27 @@
|
||||
package org.pixeldroid.app.utils.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import androidx.room.*
|
||||
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
||||
|
||||
@Dao
|
||||
interface InstanceDao {
|
||||
@Query("SELECT * FROM instances")
|
||||
fun getAll(): List<InstanceDatabaseEntity>
|
||||
|
||||
@Query("SELECT * FROM instances WHERE uri=:instanceUri")
|
||||
fun getInstance(instanceUri: String): InstanceDatabaseEntity
|
||||
|
||||
|
||||
@Query("SELECT * FROM instances WHERE uri=(SELECT users.instance_uri FROM users WHERE isActive=1)")
|
||||
fun getActiveInstance(): InstanceDatabaseEntity
|
||||
|
||||
/**
|
||||
* Insert an instance, if it already exists return -1
|
||||
*/
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertInstance(instance: InstanceDatabaseEntity): Long
|
||||
fun insertInstance(instance: InstanceDatabaseEntity): Long
|
||||
|
||||
@Update
|
||||
suspend fun updateInstance(instance: InstanceDatabaseEntity)
|
||||
fun updateInstance(instance: InstanceDatabaseEntity)
|
||||
|
||||
@Transaction
|
||||
suspend fun insertOrUpdate(instance: InstanceDatabaseEntity) {
|
||||
fun insertOrUpdate(instance: InstanceDatabaseEntity) {
|
||||
if (insertInstance(instance) == -1L) {
|
||||
updateInstance(instance)
|
||||
}
|
||||
|
@ -1,36 +0,0 @@
|
||||
package org.pixeldroid.app.utils.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import org.pixeldroid.app.utils.db.entities.TabsDatabaseEntity
|
||||
|
||||
@Dao
|
||||
interface TabsDao {
|
||||
@Query("SELECT * FROM tabsChecked WHERE `index`=:index AND `user_id`=:userId AND `instance_uri`=:instanceUri")
|
||||
fun getTabChecked(index: Int, userId: String, instanceUri: String): TabsDatabaseEntity?
|
||||
|
||||
@Query("SELECT * FROM tabsChecked WHERE `user_id`=:userId AND `instance_uri`=:instanceUri")
|
||||
fun getTabsChecked(userId: String, instanceUri: String): List<TabsDatabaseEntity>
|
||||
|
||||
@Query("DELETE FROM tabsChecked WHERE `index`=:index AND `user_id`=:userId AND `instance_uri`=:instanceUri")
|
||||
fun deleteTabChecked(index: Int, userId: String, instanceUri: String)
|
||||
|
||||
@Query("DELETE FROM tabsChecked WHERE `user_id`=:userId AND `instance_uri`=:instanceUri")
|
||||
fun deleteTabsChecked(userId: String, instanceUri: String)
|
||||
|
||||
/**
|
||||
* Insert a tab, if it already exists return -1
|
||||
*/
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertTabChecked(tabChecked: TabsDatabaseEntity): Long
|
||||
|
||||
@Transaction
|
||||
suspend fun clearAndRefill(tabsChecked: List<TabsDatabaseEntity>, userId: String, instanceUri: String) {
|
||||
deleteTabsChecked(userId, instanceUri)
|
||||
tabsChecked.forEach { insertTabChecked(it) }
|
||||
}
|
||||
}
|
@ -1,12 +1,6 @@
|
||||
package org.pixeldroid.app.utils.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import androidx.room.*
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
|
||||
@Dao
|
||||
@ -15,21 +9,17 @@ interface UserDao {
|
||||
* Insert a user, if it already exists return -1
|
||||
*/
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertUser(user: UserDatabaseEntity): Long
|
||||
fun insertUser(user: UserDatabaseEntity): Long
|
||||
|
||||
@Transaction
|
||||
suspend fun insertOrUpdate(user: UserDatabaseEntity) {
|
||||
fun insertOrUpdate(user: UserDatabaseEntity) {
|
||||
if (insertUser(user) == -1L) {
|
||||
updateUser(user)
|
||||
}
|
||||
}
|
||||
|
||||
@Update
|
||||
suspend fun updateUser(user: UserDatabaseEntity)
|
||||
|
||||
@Query("UPDATE users SET username = :username, display_name = :displayName, avatar_static = :avatarStatic WHERE user_id = :id and instance_uri = :instanceUri")
|
||||
suspend fun updateUserAccountDetails(username: String, displayName: String, avatarStatic: String, id: String, instanceUri: String)
|
||||
|
||||
fun updateUser(user: UserDatabaseEntity)
|
||||
|
||||
@Query("UPDATE users SET accessToken = :accessToken, refreshToken = :refreshToken WHERE user_id = :id and instance_uri = :instanceUri")
|
||||
fun updateAccessToken(accessToken: String, refreshToken: String, id: String, instanceUri: String)
|
||||
@ -37,9 +27,6 @@ interface UserDao {
|
||||
@Query("SELECT * FROM users")
|
||||
fun getAll(): List<UserDatabaseEntity>
|
||||
|
||||
@Query("SELECT * FROM users")
|
||||
fun getAllFlow(): Flow<List<UserDatabaseEntity>>
|
||||
|
||||
@Query("SELECT * FROM users WHERE isActive=1")
|
||||
fun getActiveUser(): UserDatabaseEntity?
|
||||
|
||||
|
@ -1,25 +0,0 @@
|
||||
package org.pixeldroid.app.utils.db.dao.feedContent
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import org.pixeldroid.app.utils.api.objects.Conversation
|
||||
import org.pixeldroid.app.utils.api.objects.Notification
|
||||
import org.pixeldroid.app.utils.db.entities.DirectMessageDatabaseEntity
|
||||
|
||||
@Dao
|
||||
interface DirectMessagesConversationDao: FeedContentDao<DirectMessageDatabaseEntity> {
|
||||
|
||||
@Query("DELETE FROM directMessagesThreads WHERE user_id=:userId AND instance_uri=:instanceUri AND conversationsId=:conversationsId")
|
||||
override suspend fun clearFeedContent(userId: String, instanceUri: String, conversationsId: String)
|
||||
|
||||
//TODO think about ordering
|
||||
@Query("SELECT * FROM directMessagesThreads WHERE user_id=:userId AND instance_uri=:instanceUri AND conversationsId=:conversationsId ORDER BY datetime(created_at) DESC")
|
||||
override fun feedContent(userId: String, instanceUri: String, conversationsId: String): PagingSource<Int, DirectMessageDatabaseEntity>
|
||||
|
||||
@Query("DELETE FROM directMessagesThreads WHERE user_id=:userId AND instance_uri=:instanceUri AND id=:id AND conversationsId=:conversationsId")
|
||||
override suspend fun delete(id: String, userId: String, instanceUri: String, conversationsId: String)
|
||||
|
||||
@Query("SELECT id FROM directMessagesThreads WHERE user_id=:userId AND instance_uri=:instanceUri AND conversationsId=:conversationsId ORDER BY datetime(created_at) ASC LIMIT 1")
|
||||
suspend fun lastMessageId(userId: String, instanceUri: String, conversationsId: String): String?
|
||||
}
|
@ -4,17 +4,16 @@ import androidx.paging.PagingSource
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import org.pixeldroid.app.utils.api.objects.Conversation
|
||||
import org.pixeldroid.app.utils.api.objects.Notification
|
||||
import org.pixeldroid.app.utils.db.dao.feedContent.FeedContentDao
|
||||
|
||||
@Dao
|
||||
interface DirectMessagesDao: FeedContentDao<Conversation> {
|
||||
|
||||
@Query("DELETE FROM directMessages WHERE user_id=:userId AND instance_uri=:instanceUri AND :conversationsId=''")
|
||||
override suspend fun clearFeedContent(userId: String, instanceUri: String, conversationsId: String)
|
||||
@Query("DELETE FROM direct_messages WHERE user_id=:userId AND instance_uri=:instanceUri")
|
||||
override suspend fun clearFeedContent(userId: String, instanceUri: String)
|
||||
|
||||
//TODO think about ordering
|
||||
@Query("""SELECT * FROM directMessages WHERE user_id=:userId AND instance_uri=:instanceUri AND :conversationsId=""""")
|
||||
override fun feedContent(userId: String, instanceUri: String, conversationsId: String): PagingSource<Int, Conversation>
|
||||
|
||||
@Query("DELETE FROM directMessages WHERE user_id=:userId AND instance_uri=:instanceUri AND id=:id AND :conversationsId=''")
|
||||
override suspend fun delete(id: String, userId: String, instanceUri: String, conversationsId: String)
|
||||
// TODO: might have to order by date or some other value
|
||||
@Query("""SELECT * FROM direct_messages WHERE user_id=:userId AND instance_uri=:instanceUri """)
|
||||
override fun feedContent(userId: String, instanceUri: String): PagingSource<Int, Conversation>
|
||||
}
|
@ -7,14 +7,10 @@ import org.pixeldroid.app.utils.api.objects.FeedContentDatabase
|
||||
|
||||
interface FeedContentDao<T: FeedContentDatabase>{
|
||||
|
||||
fun feedContent(userId: String, instanceUri: String, conversationsId: String): PagingSource<Int, T>
|
||||
fun feedContent(userId: String, instanceUri: String): PagingSource<Int, T>
|
||||
|
||||
suspend fun clearFeedContent(userId: String, instanceUri: String, conversationsId: String)
|
||||
suspend fun clearFeedContent(userId: String, instanceUri: String) = clearFeedContent(userId, instanceUri, "")
|
||||
suspend fun clearFeedContent(userId: String, instanceUri: String)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertAll(feedContent: List<T>)
|
||||
|
||||
suspend fun delete(id: String, userId: String, instanceUri: String, conversationsId: String)
|
||||
suspend fun delete(id: String, userId: String, instanceUri: String) = delete(id, userId, instanceUri, "")
|
||||
}
|
@ -8,17 +8,14 @@ import org.pixeldroid.app.utils.api.objects.Notification
|
||||
@Dao
|
||||
interface NotificationDao: FeedContentDao<Notification> {
|
||||
|
||||
@Query("DELETE FROM notifications WHERE user_id=:userId AND instance_uri=:instanceUri AND :conversationsId=''")
|
||||
override suspend fun clearFeedContent(userId: String, instanceUri: String, conversationsId: String)
|
||||
@Query("DELETE FROM notifications WHERE user_id=:userId AND instance_uri=:instanceUri")
|
||||
override suspend fun clearFeedContent(userId: String, instanceUri: String)
|
||||
|
||||
@Query("""SELECT * FROM notifications WHERE user_id=:userId AND instance_uri=:instanceUri AND :conversationsId=""
|
||||
@Query("""SELECT * FROM notifications WHERE user_id=:userId AND instance_uri=:instanceUri
|
||||
ORDER BY datetime(created_at) DESC""")
|
||||
override fun feedContent(userId: String, instanceUri: String, conversationsId: String): PagingSource<Int, Notification>
|
||||
override fun feedContent(userId: String, instanceUri: String): PagingSource<Int, Notification>
|
||||
|
||||
@Query("""SELECT * FROM notifications WHERE user_id=:userId AND instance_uri=:instanceUri
|
||||
ORDER BY datetime(created_at) DESC LIMIT 1""")
|
||||
fun latestNotification(userId: String, instanceUri: String): Notification?
|
||||
|
||||
@Query("DELETE FROM notifications WHERE user_id=:userId AND instance_uri=:instanceUri AND id=:id AND :conversationsId=''")
|
||||
override suspend fun delete(id: String, userId: String, instanceUri: String, conversationsId: String)
|
||||
}
|
@ -8,15 +8,15 @@ import org.pixeldroid.app.utils.db.entities.HomeStatusDatabaseEntity
|
||||
|
||||
@Dao
|
||||
interface HomePostDao: FeedContentDao<HomeStatusDatabaseEntity> {
|
||||
@Query("""SELECT * FROM homePosts WHERE user_id=:userId AND instance_uri=:instanceUri AND :conversationsId=""
|
||||
@Query("""SELECT * FROM homePosts WHERE user_id=:userId AND instance_uri=:instanceUri
|
||||
ORDER BY datetime(created_at) DESC""")
|
||||
override fun feedContent(userId: String, instanceUri: String, conversationsId: String): PagingSource<Int, HomeStatusDatabaseEntity>
|
||||
override fun feedContent(userId: String, instanceUri: String): PagingSource<Int, HomeStatusDatabaseEntity>
|
||||
|
||||
@Query("DELETE FROM homePosts WHERE user_id=:userId AND instance_uri=:instanceUri AND :conversationsId=''")
|
||||
override suspend fun clearFeedContent(userId: String, instanceUri: String, conversationsId: String)
|
||||
@Query("DELETE FROM homePosts WHERE user_id=:userId AND instance_uri=:instanceUri")
|
||||
override suspend fun clearFeedContent(userId: String, instanceUri: String)
|
||||
|
||||
@Query("DELETE FROM homePosts WHERE user_id=:userId AND instance_uri=:instanceUri AND id=:id AND :conversationsId=''")
|
||||
override suspend fun delete(id: String, userId: String, instanceUri: String, conversationsId: String)
|
||||
@Query("DELETE FROM homePosts WHERE user_id=:userId AND instance_uri=:instanceUri AND id=:id")
|
||||
suspend fun delete(id: String, userId: String, instanceUri: String)
|
||||
|
||||
@Query("UPDATE homePosts SET bookmarked=:bookmarked WHERE user_id=:id AND instance_uri=:instanceUri AND id=:statusId")
|
||||
fun bookmarkStatus(id: String, instanceUri: String, statusId: String, bookmarked: Boolean)
|
||||
|
@ -8,15 +8,15 @@ import org.pixeldroid.app.utils.db.entities.PublicFeedStatusDatabaseEntity
|
||||
|
||||
@Dao
|
||||
interface PublicPostDao: FeedContentDao<PublicFeedStatusDatabaseEntity> {
|
||||
@Query("""SELECT * FROM publicPosts WHERE user_id=:userId AND instance_uri=:instanceUri AND :conversationsId=""
|
||||
@Query("""SELECT * FROM publicPosts WHERE user_id=:userId AND instance_uri=:instanceUri
|
||||
ORDER BY datetime(created_at) DESC""")
|
||||
override fun feedContent(userId: String, instanceUri: String, conversationsId: String): PagingSource<Int, PublicFeedStatusDatabaseEntity>
|
||||
override fun feedContent(userId: String, instanceUri: String): PagingSource<Int, PublicFeedStatusDatabaseEntity>
|
||||
|
||||
@Query("DELETE FROM publicPosts WHERE user_id=:userId AND instance_uri=:instanceUri AND :conversationsId=''")
|
||||
override suspend fun clearFeedContent(userId: String, instanceUri: String, conversationsId: String)
|
||||
@Query("DELETE FROM publicPosts WHERE user_id=:userId AND instance_uri=:instanceUri")
|
||||
override suspend fun clearFeedContent(userId: String, instanceUri: String)
|
||||
|
||||
@Query("DELETE FROM publicPosts WHERE user_id=:userId AND instance_uri=:instanceUri AND id=:id AND :conversationsId=''")
|
||||
override suspend fun delete(id: String, userId: String, instanceUri: String, conversationsId: String)
|
||||
@Query("DELETE FROM publicPosts WHERE user_id=:userId AND instance_uri=:instanceUri AND id=:id")
|
||||
override suspend fun delete(id: String, userId: String, instanceUri: String)
|
||||
|
||||
@Query("UPDATE homePosts SET bookmarked=:bookmarked WHERE user_id=:id AND instance_uri=:instanceUri AND id=:statusId")
|
||||
fun bookmarkStatus(id: String, instanceUri: String, statusId: String, bookmarked: Boolean)
|
||||
|
@ -1,62 +0,0 @@
|
||||
package org.pixeldroid.app.utils.db.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import org.pixeldroid.app.utils.api.objects.Attachment
|
||||
import org.pixeldroid.app.utils.api.objects.FeedContent
|
||||
import org.pixeldroid.app.utils.api.objects.FeedContentDatabase
|
||||
import org.pixeldroid.app.utils.api.objects.Message
|
||||
import java.io.Serializable
|
||||
import java.time.Instant
|
||||
|
||||
@Entity(
|
||||
tableName = "directMessagesThreads",
|
||||
primaryKeys = ["id", "conversationsId", "user_id", "instance_uri"],
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = UserDatabaseEntity::class,
|
||||
parentColumns = arrayOf("user_id", "instance_uri"),
|
||||
childColumns = arrayOf("user_id", "instance_uri"),
|
||||
onUpdate = ForeignKey.CASCADE,
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)],
|
||||
indices = [Index(value = ["user_id", "instance_uri", "conversationsId"])]
|
||||
)
|
||||
data class DirectMessageDatabaseEntity(
|
||||
override val id: String,
|
||||
val name: String?,
|
||||
val hidden: Boolean?,
|
||||
val isAuthor: Boolean?,
|
||||
val type: String?, //TODO enum?
|
||||
val text: String?,
|
||||
val media: String?, //TODO,
|
||||
val carousel: List<Attachment>?,
|
||||
val created_at: Instant?, //ISO 8601 Datetime
|
||||
val timeAgo: String?,
|
||||
val reportId: String?,
|
||||
//val meta: String?, //TODO
|
||||
|
||||
// Database values (not from API)
|
||||
val conversationsId: String,
|
||||
override var user_id: String,
|
||||
override var instance_uri: String,
|
||||
): FeedContent, FeedContentDatabase, Serializable {
|
||||
constructor(message: Message, conversationsId: String, user: UserDatabaseEntity) : this(
|
||||
message.id,
|
||||
message.name,
|
||||
message.hidden,
|
||||
message.isAuthor,
|
||||
message.type,
|
||||
message.text,
|
||||
message.media,
|
||||
message.carousel,
|
||||
message.created_at,
|
||||
message.timeAgo,
|
||||
message.reportId,
|
||||
//message.meta,
|
||||
|
||||
conversationsId,
|
||||
user.user_id,
|
||||
user.instance_uri
|
||||
)
|
||||
}
|
@ -4,22 +4,20 @@ import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "instances")
|
||||
data class InstanceDatabaseEntity(
|
||||
@PrimaryKey var uri: String,
|
||||
var title: String,
|
||||
var maxStatusChars: Int = DEFAULT_MAX_TOOT_CHARS,
|
||||
// Per-file file-size limit in KB. Defaults to 15000 (15MB). Default limit for Mastodon is 8MB
|
||||
var maxPhotoSize: Int = DEFAULT_MAX_PHOTO_SIZE,
|
||||
// Mastodon has different file limits for videos, default of 40MB
|
||||
var maxVideoSize: Int = DEFAULT_MAX_VIDEO_SIZE,
|
||||
// How many photos can go into an album. Default limit for Pixelfed and Mastodon is 4
|
||||
var albumLimit: Int = DEFAULT_ALBUM_LIMIT,
|
||||
// Is video functionality enabled on this instance?
|
||||
var videoEnabled: Boolean = DEFAULT_VIDEO_ENABLED,
|
||||
// Is this Pixelfed instance?
|
||||
var pixelfed: Boolean = true,
|
||||
data class InstanceDatabaseEntity (
|
||||
@PrimaryKey var uri: String,
|
||||
var title: String,
|
||||
var maxStatusChars: Int = DEFAULT_MAX_TOOT_CHARS,
|
||||
// Per-file file-size limit in KB. Defaults to 15000 (15MB). Default limit for Mastodon is 8MB
|
||||
var maxPhotoSize: Int = DEFAULT_MAX_PHOTO_SIZE,
|
||||
// Mastodon has different file limits for videos, default of 40MB
|
||||
var maxVideoSize: Int = DEFAULT_MAX_VIDEO_SIZE,
|
||||
// How many photos can go into an album. Default limit for Pixelfed and Mastodon is 4
|
||||
var albumLimit: Int = DEFAULT_ALBUM_LIMIT,
|
||||
// Is video functionality enabled on this instance?
|
||||
var videoEnabled: Boolean = DEFAULT_VIDEO_ENABLED,
|
||||
) {
|
||||
companion object {
|
||||
companion object{
|
||||
// Default max number of chars for Mastodon: used when their is no other value supplied by
|
||||
// either NodeInfo or the instance endpoint
|
||||
const val DEFAULT_MAX_TOOT_CHARS = 500
|
||||
|
@ -1,26 +0,0 @@
|
||||
package org.pixeldroid.app.utils.db.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
|
||||
@Entity(
|
||||
tableName = "tabsChecked",
|
||||
primaryKeys = ["index", "user_id", "instance_uri"],
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = UserDatabaseEntity::class,
|
||||
parentColumns = arrayOf("user_id", "instance_uri"),
|
||||
childColumns = arrayOf("user_id", "instance_uri"),
|
||||
onUpdate = ForeignKey.CASCADE,
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)],
|
||||
indices = [Index(value = ["user_id", "instance_uri"])]
|
||||
)
|
||||
data class TabsDatabaseEntity(
|
||||
var index: Int,
|
||||
var user_id: String,
|
||||
var instance_uri: String,
|
||||
var tab: String,
|
||||
var checked: Boolean = true,
|
||||
var filter: String? = null
|
||||
)
|
@ -6,16 +6,13 @@ import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.*
|
||||
import org.pixeldroid.app.utils.api.PixelfedAPI.Companion.apiForUser
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class APIModule {
|
||||
class APIModule{
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@ -57,7 +54,7 @@ class TokenAuthenticator(val user: UserDatabaseEntity, val db: AppDatabase, val
|
||||
client_secret = user.clientSecret
|
||||
)
|
||||
}
|
||||
} catch (e: Exception){
|
||||
}catch (e: Exception){
|
||||
return null
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,34 @@
|
||||
package org.pixeldroid.app.utils.di
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.PixelDroidApplication
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.BaseFragment
|
||||
import dagger.Component
|
||||
import org.pixeldroid.app.directMessages.ui.main.ConversationsViewModel
|
||||
import org.pixeldroid.app.postCreation.PostCreationViewModel
|
||||
import org.pixeldroid.app.profile.EditProfileViewModel
|
||||
import org.pixeldroid.app.stories.StoriesViewModel
|
||||
import org.pixeldroid.app.stories.StoryCarouselViewHolder
|
||||
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
||||
@Singleton
|
||||
@Component(modules = [ApplicationModule::class, DatabaseModule::class, APIModule::class])
|
||||
interface ApplicationComponent {
|
||||
fun inject(application: PixelDroidApplication?)
|
||||
fun inject(activity: BaseActivity?)
|
||||
fun inject(feedFragment: BaseFragment)
|
||||
fun inject(notificationsWorker: NotificationsWorker)
|
||||
fun inject(postCreationViewModel: PostCreationViewModel)
|
||||
fun inject(editProfileViewModel: EditProfileViewModel)
|
||||
fun inject(editProfileViewModel: ConversationsViewModel)
|
||||
fun inject(storiesViewModel: StoriesViewModel)
|
||||
|
||||
val context: Context?
|
||||
val application: Application?
|
||||
val database: AppDatabase
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package org.pixeldroid.app.utils.di
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
||||
@Module
|
||||
class ApplicationModule(app: Application) {
|
||||
private val mApplication: Application = app
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideContext(): Context {
|
||||
return mApplication
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideApplication(): Application {
|
||||
return mApplication
|
||||
}
|
||||
|
||||
}
|
@ -5,28 +5,20 @@ import androidx.room.Room
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.pixeldroid.app.utils.db.MIGRATION_3_4
|
||||
import org.pixeldroid.app.utils.db.MIGRATION_4_5
|
||||
import org.pixeldroid.app.utils.db.MIGRATION_5_6
|
||||
import org.pixeldroid.app.utils.db.MIGRATION_9_10
|
||||
import javax.inject.Singleton
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
class DatabaseModule {
|
||||
class DatabaseModule(private val context: Context) {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesDatabase(
|
||||
@ApplicationContext applicationContext: Context
|
||||
): AppDatabase {
|
||||
fun providesDatabase(): AppDatabase {
|
||||
return Room.databaseBuilder(
|
||||
applicationContext,
|
||||
context,
|
||||
AppDatabase::class.java, "pixeldroid"
|
||||
).addMigrations(MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_9_10)
|
||||
).addMigrations(MIGRATION_3_4).addMigrations(MIGRATION_4_5)
|
||||
.allowMainThreadQueries().build()
|
||||
}
|
||||
}
|
@ -12,42 +12,43 @@ import android.os.Build
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import org.pixeldroid.app.main.MainActivity
|
||||
import org.pixeldroid.app.MainActivity
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.posts.PostActivity
|
||||
import org.pixeldroid.app.posts.fromHtml
|
||||
import org.pixeldroid.app.utils.PixelDroidApplication
|
||||
import org.pixeldroid.app.utils.api.PixelfedAPI.Companion.apiForUser
|
||||
import org.pixeldroid.app.utils.api.objects.Notification
|
||||
import org.pixeldroid.app.utils.api.objects.Notification.NotificationType.comment
|
||||
import org.pixeldroid.app.utils.api.objects.Notification.NotificationType.entries
|
||||
import org.pixeldroid.app.utils.api.objects.Notification.NotificationType.favourite
|
||||
import org.pixeldroid.app.utils.api.objects.Notification.NotificationType.follow
|
||||
import org.pixeldroid.app.utils.api.objects.Notification.NotificationType.follow_request
|
||||
import org.pixeldroid.app.utils.api.objects.Notification.NotificationType.mention
|
||||
import org.pixeldroid.app.utils.api.objects.Notification.NotificationType.poll
|
||||
import org.pixeldroid.app.utils.api.objects.Notification.NotificationType.reblog
|
||||
import org.pixeldroid.app.utils.api.objects.Notification.NotificationType.status
|
||||
import org.pixeldroid.app.utils.api.objects.Notification.NotificationType.*
|
||||
import org.pixeldroid.app.utils.api.objects.Status
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
import org.pixeldroid.app.utils.getColorFromAttr
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltWorker
|
||||
class NotificationsWorker @AssistedInject constructor(
|
||||
@Assisted context: Context,
|
||||
@Assisted params: WorkerParameters,
|
||||
private val db: AppDatabase,
|
||||
private val apiHolder: PixelfedAPIHolder
|
||||
|
||||
|
||||
|
||||
class NotificationsWorker(
|
||||
context: Context,
|
||||
params: WorkerParameters
|
||||
) : CoroutineWorker(context, params) {
|
||||
|
||||
@Inject
|
||||
lateinit var db: AppDatabase
|
||||
@Inject
|
||||
lateinit var apiHolder: PixelfedAPIHolder
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
|
||||
(applicationContext as PixelDroidApplication).getAppComponent().inject(this)
|
||||
|
||||
val users: List<UserDatabaseEntity> = db.userDao().getAll()
|
||||
|
||||
for (user in users){
|
||||
@ -305,7 +306,8 @@ fun removeNotificationChannelsFromAccount(context: Context, user: UserDatabaseEn
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
notificationManager.deleteNotificationChannelGroup(channelGroupId.hashCode().toString())
|
||||
} else {
|
||||
val types: MutableList<Notification.NotificationType?> = entries.toMutableList()
|
||||
val types: MutableList<Notification.NotificationType?> =
|
||||
Notification.NotificationType.values().toMutableList()
|
||||
types += null
|
||||
|
||||
types.forEach {
|
||||
|
@ -1,5 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||
|
||||
</vector>
|
@ -1,5 +0,0 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M20,10L20,8h-4L16,4h-2v4h-4L10,4L8,4v4L4,8v2h4v4L4,14v2h4v4h2v-4h4v4h2v-4h4v-2h-4v-4h4zM14,14h-4v-4h4v4z"/>
|
||||
</vector>
|
@ -1,5 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="?attr/colorOnBackground" android:pathData="M19,2L5,2c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h4l3,3 3,-3h4c1.1,0 2,-0.9 2,-2L21,4c0,-1.1 -0.9,-2 -2,-2zM13,18h-2v-2h2v2zM15.07,10.25l-0.9,0.92C13.45,11.9 13,12.5 13,14h-2v-0.5c0,-1.1 0.45,-2.1 1.17,-2.83l1.24,-1.26c0.37,-0.36 0.59,-0.86 0.59,-1.41 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2L8,8c0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,0.88 -0.36,1.68 -0.93,2.25z"/>
|
||||
|
||||
</vector>
|
@ -1,5 +1,5 @@
|
||||
<vector android:height="24dp"
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="?attr/colorOnBackground" android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
|
||||
<path android:fillColor="#FF000000" android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
|
||||
</vector>
|
@ -1,5 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M17,7l-1.41,1.41L18.17,11H8v2h10.17l-2.58,2.58L17,17l5,-5zM4,5h8V3H4c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h8v-2H4V5z"/>
|
||||
|
||||
</vector>
|
@ -1,5 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="?attr/colorOnBackground" android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM18,14L6,14v-2h12v2zM18,11L6,11L6,9h12v2zM18,8L6,8L6,6h12v2z"/>
|
||||
|
||||
</vector>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user