diff --git a/app/build.gradle b/app/build.gradle
index 7c919df1..faa3001a 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -46,7 +46,7 @@ android {
defaultConfig {
minSdkVersion 23
targetSdkVersion 34
- versionCode 33
+ versionCode 34
versionName "1.0.beta" + versionCode
//TODO add resConfigs("en", "fr", "ja",...) ?
@@ -164,15 +164,17 @@ android {
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.0.4'
/**
* AndroidX dependencies:
*/
- implementation 'androidx.appcompat:appcompat:1.6.1'
+ implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.core:core-splashscreen:1.0.1'
- implementation 'androidx.core:core-ktx:1.12.0'
+ implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
@@ -182,23 +184,23 @@ dependencies {
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
- implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
- implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0'
- implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
- implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.7.0'
- implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
- implementation "androidx.lifecycle:lifecycle-common-java8:2.7.0"
- implementation "androidx.annotation:annotation:1.7.1"
+ implementation 'androidx.paging:paging-runtime-ktx:3.3.2'
+ implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.4'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.4'
+ implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.4'
+ implementation "androidx.lifecycle:lifecycle-common-java8:2.8.4"
+ implementation "androidx.annotation:annotation:1.8.2"
implementation 'androidx.gridlayout:gridlayout:1.0.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.activity:activity-ktx:1.9.1"
+ implementation 'androidx.fragment:fragment-ktx:1.8.2'
+ implementation 'androidx.work:work-runtime-ktx:2.9.1'
implementation 'androidx.media2:media2-widget:1.3.0'
implementation 'androidx.media2:media2-player:1.3.0'
// Use the most recent version of CameraX
- def cameraX_version = '1.3.2'
+ def cameraX_version = '1.3.4'
implementation "androidx.camera:camera-core:$cameraX_version"
implementation "androidx.camera:camera-camera2:$cameraX_version"
// CameraX Lifecycle library
@@ -220,11 +222,11 @@ dependencies {
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
- implementation 'com.google.android.material:material:1.11.0'
+ implementation 'com.google.android.material:material:1.12.0'
//Dagger (dependency injection)
- implementation 'com.google.dagger:dagger:2.51'
- ksp 'com.google.dagger:dagger-compiler:2.51'
+ implementation 'com.google.dagger:dagger:2.51.1'
+ ksp 'com.google.dagger:dagger-compiler:2.51.1'
implementation('com.google.dagger:hilt-android:2.51')
ksp 'com.google.dagger:hilt-compiler:2.51'
@@ -237,7 +239,7 @@ dependencies {
implementation 'io.reactivex.rxjava3:rxandroid:3.0.2'
implementation 'com.github.connyduck:sparkbutton:4.1.0'
- implementation 'org.pixeldroid.pixeldroid:android-media-editor:2.0'
+ implementation 'org.pixeldroid.pixeldroid:android-media-editor:3.1'
implementation project(path: ':scrambler')
implementation project(path: ':pixel_common')
@@ -260,12 +262,6 @@ dependencies {
// Add for NavController support
implementation 'com.mikepenz:materialdrawer-nav:9.0.2'
- //iconics
- implementation 'com.mikepenz:iconics-core:5.4.0'
- implementation 'com.mikepenz:materialdrawer-iconics:9.0.2'
- 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'
implementation 'me.relex:circleindicator:2.1.6'
@@ -280,7 +276,7 @@ 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.9.0'
+ androidTestImplementation 'androidx.work:work-testing:2.9.1'
testImplementation 'com.github.tomakehurst:wiremock-jre8:2.34.0'
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
testImplementation 'junit:junit:4.13.2'
@@ -288,13 +284,13 @@ dependencies {
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.3.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 'androidx.test:runner:1.6.1'
+ androidTestImplementation 'androidx.test:rules:1.6.1'
+ androidTestImplementation 'androidx.test.ext:junit:1.2.1'
+ androidTestImplementation 'androidx.test:runner:1.6.1'
+ 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 'com.android.support.test.espresso:espresso-contrib:3.0.2'
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ca79bcca..a5608ec5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -53,9 +53,7 @@
android:theme="@style/BaseAppTheme"/>
+ android:theme="@style/BaseAppTheme" />
@@ -76,39 +74,29 @@
+ android:theme="@style/BaseAppTheme" />
+ android:theme="@style/BaseAppTheme" />
+ android:windowSoftInputMode="adjustPan">
@@ -122,12 +110,10 @@
android:resource="@xml/shortcuts" />
+ android:windowSoftInputMode="adjustResize">
@@ -143,9 +129,7 @@
android:name=".searchDiscover.SearchActivity"
android:exported="true"
android:theme="@style/BaseAppTheme"
- android:launchMode="singleTop"
- android:screenOrientation="sensorPortrait"
- tools:ignore="LockedOrientationActivity">
+ android:launchMode="singleTop">
@@ -166,6 +150,16 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/LoginActivity.kt b/app/src/main/java/org/pixeldroid/app/LoginActivity.kt
deleted file mode 100644
index e087d98a..00000000
--- a/app/src/main/java/org/pixeldroid/app/LoginActivity.kt
+++ /dev/null
@@ -1,345 +0,0 @@
-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 = 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)
- )
- }
-
-}
diff --git a/app/src/main/java/org/pixeldroid/app/login/LoginActivity.kt b/app/src/main/java/org/pixeldroid/app/login/LoginActivity.kt
new file mode 100644
index 00000000..f62d3c20
--- /dev/null
+++ b/app/src/main/java/org/pixeldroid/app/login/LoginActivity.kt
@@ -0,0 +1,189 @@
+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.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.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 {
+ if (it) {
+ val intent = Intent(this@LoginActivity, MainActivity::class.java)
+ intent.flags =
+ Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ startActivity(intent)
+ }
+ }
+ }
+ }
+ 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
+ }
+ }
+
+}
diff --git a/app/src/main/java/org/pixeldroid/app/login/LoginActivityViewModel.kt b/app/src/main/java/org/pixeldroid/app/login/LoginActivityViewModel.kt
new file mode 100644
index 00000000..91f58a0a
--- /dev/null
+++ b/app/src/main/java/org/pixeldroid/app/login/LoginActivityViewModel.kt
@@ -0,0 +1,283 @@
+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 = MutableStateFlow(LoginState(LoginState.LoadingState.Resting))
+ val loadingState = _loadingState.asStateFlow()
+
+ private val _finishedLogin = MutableStateFlow(false)
+ val finishedLogin = _finishedLogin.asStateFlow()
+
+ private val _promptOauth: MutableStateFlow = 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 = 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 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(R.string.verify_credentials)
+ }
+
+ fetchNotifications()
+ _finishedLogin.value = true
+ }
+
+ // 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)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/main/AccountListAdapter.kt b/app/src/main/java/org/pixeldroid/app/main/AccountListAdapter.kt
new file mode 100644
index 00000000..8f6fc60e
--- /dev/null
+++ b/app/src/main/java/org/pixeldroid/app/main/AccountListAdapter.kt
@@ -0,0 +1,62 @@
+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>,
+ lifecycleScope: LifecycleCoroutineScope,
+ private val onClick: (UserDatabaseEntity?) -> Unit
+) : RecyclerView.Adapter() {
+ private val itemsList: MutableList = 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)
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/MainActivity.kt b/app/src/main/java/org/pixeldroid/app/main/MainActivity.kt
similarity index 74%
rename from app/src/main/java/org/pixeldroid/app/MainActivity.kt
rename to app/src/main/java/org/pixeldroid/app/main/MainActivity.kt
index 6912fbe2..d1cfa6ac 100644
--- a/app/src/main/java/org/pixeldroid/app/MainActivity.kt
+++ b/app/src/main/java/org/pixeldroid/app/main/MainActivity.kt
@@ -1,4 +1,4 @@
-package org.pixeldroid.app
+package org.pixeldroid.app.main
import android.Manifest
import android.content.Context
@@ -16,31 +16,37 @@ 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.core.view.marginEnd
+import androidx.core.view.marginTop
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.GridLayoutManager
+import androidx.recyclerview.widget.LinearLayoutManager
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.bumptech.glide.request.RequestOptions
-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.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
@@ -49,6 +55,8 @@ 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.login.LoginActivity
+import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityMainBinding
import org.pixeldroid.app.postCreation.camera.CameraFragment
import org.pixeldroid.app.posts.NestedScrollableHost
@@ -70,6 +78,7 @@ import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companio
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
@@ -179,22 +188,53 @@ class MainActivity : BaseActivity() {
}
private fun setupDrawer() {
- binding.mainDrawerButton.setOnClickListener{
- binding.drawerLayout.openDrawer(binding.drawer)
+ binding.mainDrawerButton?.setOnClickListener {
+ binding.drawer?.let { drawer -> binding.drawerLayout.openDrawer(drawer) }
}
- header = AccountHeaderView(this).apply {
- attachToSliderView(binding.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 ->
- clickProfile(profile, current)
+ 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
- iconicsIcon = GoogleMaterial.Icon.gmd_add
+ iconRes = R.drawable.add
}, 0)
dividerBelowHeader = false
closeDrawerOnProfileListClick = true
@@ -202,11 +242,11 @@ class MainActivity : BaseActivity() {
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)
+ Glide.with(this@MainActivity)
+ .load(uri)
+ .placeholder(placeholder)
+ .circleCrop()
+ .into(imageView)
}
override fun cancel(imageView: ImageView) {
@@ -228,22 +268,23 @@ class MainActivity : BaseActivity() {
//with the received one. This happens asynchronously.
getUpdatedAccount()
- binding.drawer.itemAdapter.add(
+ binding.drawer?.itemAdapter?.add(
primaryDrawerItem {
nameRes = R.string.menu_account
- iconicsIcon = GoogleMaterial.Icon.gmd_person
+ iconRes = R.drawable.person
},
primaryDrawerItem {
nameRes = R.string.menu_settings
- iconicsIcon = GoogleMaterial.Icon.gmd_settings
+ iconRes = R.drawable.settings
},
primaryDrawerItem {
nameRes = R.string.logout
- iconicsIcon = GoogleMaterial.Icon.gmd_close
+ iconRes = R.drawable.logout
},
)
- binding.drawer.onDrawerItemClickListener = { v, drawerItem, position ->
- when (position){
+
+ binding.drawer?.onDrawerItemClickListener = { v, drawerItem, position ->
+ when (position) {
1 -> launchActivity(ProfileActivity())
2 -> launchActivity(SettingsActivity())
3 -> logOut()
@@ -254,10 +295,9 @@ class MainActivity : BaseActivity() {
// 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)){
+ if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
binding.drawerLayout.closeDrawer(GravityCompat.START)
- }
- else {
+ } else {
this.isEnabled = false
super.onBackPressedDispatcher.onBackPressed()
}
@@ -305,18 +345,19 @@ class MainActivity : BaseActivity() {
}
//called when switching profiles, or when clicking on current profile
- private fun clickProfile(profile: IProfile, current: Boolean): Boolean {
+ @Suppress("SameReturnValue")
+ private fun clickProfile(id: String?, instance: String?, current: Boolean): Boolean {
if(current){
launchActivity(ProfileActivity())
return false
}
//Clicked on add new account
- if(profile.identifier == ADD_ACCOUNT_IDENTIFIER){
+ if(id == null || instance == null){
launchActivity(LoginActivity())
return false
}
- switchUser(profile.identifier.toString(), profile.tag as String)
+ switchUser(id, instance)
val intent = Intent(this, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
@@ -396,6 +437,34 @@ class MainActivity : BaseActivity() {
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()
+ }
+ }
+
private fun setupTabs(tab_array: List<() -> Fragment>){
binding.viewPager.reduceDragSensitivity()
binding.viewPager.adapter = object : FragmentStateAdapter(this) {
@@ -422,37 +491,49 @@ class MainActivity : BaseActivity() {
else -> null
}
if (selected != null) {
- binding.tabs.selectedItemId = selected
+ // 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.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
+
+ fun MenuItem.buttonPos() {
+ when(itemId){
+ R.id.my_profile -> launchActivity(ProfileActivity())
+ R.id.settings -> launchActivity(SettingsActivity())
+ R.id.log_out -> logOut()
}
}
- binding.tabs.setOnItemSelectedListener {item ->
+ (binding.tabs as? NavigationBarView)?.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()
+ (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
@@ -479,20 +560,13 @@ class MainActivity : BaseActivity() {
}
}
- private fun setNotificationBadge(show: Boolean, count: Int? = null){
+ 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.getOrCreateBadge(R.id.page_4)
- if (count != null) badge.number = count
+ val badge = (binding.tabs as? NavigationBarView)?.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)
+ else (binding.tabs as? NavigationBarView)?.removeBadge(R.id.page_4)
}
/**
diff --git a/app/src/main/java/org/pixeldroid/app/MainActivityViewModel.kt b/app/src/main/java/org/pixeldroid/app/main/MainActivityViewModel.kt
similarity index 97%
rename from app/src/main/java/org/pixeldroid/app/MainActivityViewModel.kt
rename to app/src/main/java/org/pixeldroid/app/main/MainActivityViewModel.kt
index 60d41a03..647f4a70 100644
--- a/app/src/main/java/org/pixeldroid/app/MainActivityViewModel.kt
+++ b/app/src/main/java/org/pixeldroid/app/main/MainActivityViewModel.kt
@@ -1,4 +1,4 @@
-package org.pixeldroid.app
+package org.pixeldroid.app.main
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt
index 993030ef..7bcbc467 100644
--- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt
+++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt
@@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import okhttp3.MultipartBody
-import org.pixeldroid.app.MainActivity
+import org.pixeldroid.app.main.MainActivity
import org.pixeldroid.app.R
import org.pixeldroid.app.postCreation.camera.CameraFragment
import org.pixeldroid.app.utils.api.objects.Attachment
diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraActivity.kt b/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraActivity.kt
index af4976b5..6ba0a332 100644
--- a/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraActivity.kt
+++ b/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraActivity.kt
@@ -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.MainActivity
+import org.pixeldroid.app.main.MainActivity
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityCameraBinding
import org.pixeldroid.app.postCreation.camera.CameraFragment.Companion.CAMERA_ACTIVITY
diff --git a/app/src/main/java/org/pixeldroid/app/posts/PostActivity.kt b/app/src/main/java/org/pixeldroid/app/posts/PostActivity.kt
index 266b71e0..06ca3ff3 100644
--- a/app/src/main/java/org/pixeldroid/app/posts/PostActivity.kt
+++ b/app/src/main/java/org/pixeldroid/app/posts/PostActivity.kt
@@ -6,7 +6,7 @@ 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.constraintlayout.widget.ConstraintLayout
+import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.pixeldroid.app.R
@@ -23,7 +23,7 @@ import org.pixeldroid.app.utils.api.objects.Status.Companion.VIEW_COMMENTS_TAG
import org.pixeldroid.app.utils.displayDimensionsInPx
class PostActivity : BaseActivity() {
- private lateinit var binding: ActivityPostBinding
+ lateinit var binding: ActivityPostBinding
private lateinit var commentFragment: CommentFragment
@@ -33,7 +33,7 @@ class PostActivity : BaseActivity() {
super.onCreate(savedInstanceState)
binding = ActivityPostBinding.inflate(layoutInflater)
- commentFragment = CommentFragment(binding.swipeRefreshLayout)
+ commentFragment = CommentFragment()
setContentView(binding.root)
setSupportActionBar(binding.topBar)
@@ -48,14 +48,15 @@ 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, displayDimensionsInPx(),
+ status, apiHolder, db, lifecycleScope, Pair((width*.7).toInt(), height),
requestPermissionDownloadPic, isActivity = true
)
activateCommenter()
- initCommentsFragment(domain = user?.instance_uri.orEmpty())
+ initCommentsFragment(domain = user?.instance_uri.orEmpty(), savedInstanceState)
if(viewComments || postComment){
//Scroll already down as much as possible (since comments are not loaded yet)
@@ -100,15 +101,21 @@ class PostActivity : BaseActivity() {
}
}
- private fun initCommentsFragment(domain: String) {
+ private fun initCommentsFragment(domain: String, savedInstanceState: Bundle?) {
val arguments = Bundle()
arguments.putSerializable(COMMENT_STATUS_ID, status.id)
arguments.putSerializable(COMMENT_DOMAIN, domain)
commentFragment.arguments = arguments
- supportFragmentManager.beginTransaction()
- .add(R.id.commentFragment, commentFragment).commit()
+ //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()
diff --git a/app/src/main/java/org/pixeldroid/app/posts/ReportActivity.kt b/app/src/main/java/org/pixeldroid/app/posts/ReportActivity.kt
index 88bff903..498bbeb3 100644
--- a/app/src/main/java/org/pixeldroid/app/posts/ReportActivity.kt
+++ b/app/src/main/java/org/pixeldroid/app/posts/ReportActivity.kt
@@ -2,15 +2,22 @@ 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)
@@ -24,42 +31,47 @@ class ReportActivity : BaseActivity() {
binding.reportTargetTextview.text = getString(R.string.report_target).format(status?.account?.acct)
+ binding.textInputLayout.editText?.text = model.editable
- binding.reportButton.setOnClickListener{
- binding.reportButton.visibility = View.INVISIBLE
- binding.reportProgressBar.visibility = View.VISIBLE
+ binding.textInputLayout.editText?.doAfterTextChanged { model.textChanged(it) }
- binding.textInputLayout.editText?.isEnabled = false
+ binding.reportButton.setOnClickListener {
+ model.sendReport(status, binding.textInputLayout.editText?.text.toString())
+ }
- 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)
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ model.reportSent.collect {
+ reportStatus(it)
}
}
}
}
- 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
+ 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
+ }
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/posts/ReportActivityViewModel.kt b/app/src/main/java/org/pixeldroid/app/posts/ReportActivityViewModel.kt
new file mode 100644
index 00000000..ed59a195
--- /dev/null
+++ b/app/src/main/java/org/pixeldroid/app/posts/ReportActivityViewModel.kt
@@ -0,0 +1,47 @@
+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 = 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
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt b/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt
index 8fa7fbba..0c5f1c01 100644
--- a/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt
+++ b/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt
@@ -152,8 +152,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
if(!status?.media_attachments.isNullOrEmpty()) {
setupPostPics(binding, request)
} else {
- binding.postPager.visibility = View.GONE
- binding.postIndicator.visibility = View.GONE
+ binding.postConstraint.visibility = View.GONE
}
}
diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/CachedFeedFragment.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/CachedFeedFragment.kt
index 4aa9abea..3f30c74b 100644
--- a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/CachedFeedFragment.kt
+++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/CachedFeedFragment.kt
@@ -55,16 +55,16 @@ open class CachedFeedFragment : 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) }
+// }
}
override fun onCreateView(
diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/PostFeedFragment.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/PostFeedFragment.kt
index 40a76c83..e63ced3a 100644
--- a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/PostFeedFragment.kt
+++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/PostFeedFragment.kt
@@ -40,8 +40,6 @@ class PostFeedFragment: CachedFeedFragment() {
home = requireArguments().getBoolean("home")
- adapter = PostsAdapter(requireContext().displayDimensionsInPx())
-
@Suppress("UNCHECKED_CAST")
if (home){
mediator = HomeFeedRemoteMediator(apiHolder, db) as RemoteMediator
@@ -61,6 +59,7 @@ class PostFeedFragment: CachedFeedFragment() {
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
+ adapter = PostsAdapter(requireContext().displayDimensionsInPx())
val view = super.onCreateView(inflater, container, savedInstanceState)
diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/UncachedFeedFragment.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/UncachedFeedFragment.kt
index 934055ca..c4a2acbf 100644
--- a/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/UncachedFeedFragment.kt
+++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/UncachedFeedFragment.kt
@@ -42,15 +42,15 @@ open class UncachedFeedFragment : 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) }
- }
+// // 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(
diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/comments/CommentFragment.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/comments/CommentFragment.kt
index def96386..8dead8ef 100644
--- a/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/comments/CommentFragment.kt
+++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/comments/CommentFragment.kt
@@ -28,7 +28,7 @@ import org.pixeldroid.app.utils.setProfileImageFromURL
/**
* Fragment to show a list of [Status]s, in form of comments
*/
-class CommentFragment(val swipeRefreshLayout: SwipeRefreshLayout): UncachedFeedFragment() {
+class CommentFragment: UncachedFeedFragment() {
private lateinit var id: String
private lateinit var domain: String
@@ -48,8 +48,10 @@ class CommentFragment(val swipeRefreshLayout: SwipeRefreshLayout): UncachedFeedF
savedInstanceState: Bundle?,
): View? {
-
- val view = super.onCreateView(inflater, container, savedInstanceState, swipeRefreshLayout)
+ val view = super.onCreateView(
+ inflater, container, savedInstanceState,
+ (activity as? PostActivity)?.binding?.swipeRefreshLayout
+ )
// Get the view model
@Suppress("UNCHECKED_CAST")
diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/hashtags/HashTagActivity.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/hashtags/HashTagActivity.kt
index b26420f0..277fccb3 100644
--- a/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/hashtags/HashTagActivity.kt
+++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/hashtags/HashTagActivity.kt
@@ -1,18 +1,20 @@
package org.pixeldroid.app.posts.feeds.uncachedFeeds.hashtags
import android.os.Bundle
+import androidx.fragment.app.add
+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
+import org.pixeldroid.app.posts.feeds.uncachedFeeds.accountLists.AccountListFragment
import org.pixeldroid.app.utils.BaseActivity
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 +34,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(R.id.followsFragment, args = arguments)
+ }
}
}
diff --git a/app/src/main/java/org/pixeldroid/app/profile/CollectionActivity.kt b/app/src/main/java/org/pixeldroid/app/profile/CollectionActivity.kt
index 09b2db53..287a2b11 100644
--- a/app/src/main/java/org/pixeldroid/app/profile/CollectionActivity.kt
+++ b/app/src/main/java/org/pixeldroid/app/profile/CollectionActivity.kt
@@ -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()
diff --git a/app/src/main/java/org/pixeldroid/app/profile/FollowsActivity.kt b/app/src/main/java/org/pixeldroid/app/profile/FollowsActivity.kt
index 7f431ca6..a279ade2 100644
--- a/app/src/main/java/org/pixeldroid/app/profile/FollowsActivity.kt
+++ b/app/src/main/java/org/pixeldroid/app/profile/FollowsActivity.kt
@@ -1,6 +1,8 @@
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
@@ -12,10 +14,8 @@ 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(R.id.followsFragment, args = arguments)
+ }
}
}
diff --git a/app/src/main/java/org/pixeldroid/app/settings/SettingsActivity.kt b/app/src/main/java/org/pixeldroid/app/settings/SettingsActivity.kt
index 99ecdf17..93f4e8eb 100644
--- a/app/src/main/java/org/pixeldroid/app/settings/SettingsActivity.kt
+++ b/app/src/main/java/org/pixeldroid/app/settings/SettingsActivity.kt
@@ -15,7 +15,7 @@ import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import org.pixeldroid.app.MainActivity
+import org.pixeldroid.app.main.MainActivity
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.SettingsBinding
import org.pixeldroid.common.ThemedActivity
diff --git a/app/src/main/java/org/pixeldroid/app/utils/PixelDroidApplication.kt b/app/src/main/java/org/pixeldroid/app/utils/PixelDroidApplication.kt
index 89ad951c..09cabc56 100644
--- a/app/src/main/java/org/pixeldroid/app/utils/PixelDroidApplication.kt
+++ b/app/src/main/java/org/pixeldroid/app/utils/PixelDroidApplication.kt
@@ -1,13 +1,29 @@
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
@HiltAndroidApp
-class PixelDroidApplication: Application() {
+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()
override fun onCreate() {
super.onCreate()
diff --git a/app/src/main/java/org/pixeldroid/app/utils/notificationsWorker/NotificationsWorker.kt b/app/src/main/java/org/pixeldroid/app/utils/notificationsWorker/NotificationsWorker.kt
index f4646b54..68c45961 100644
--- a/app/src/main/java/org/pixeldroid/app/utils/notificationsWorker/NotificationsWorker.kt
+++ b/app/src/main/java/org/pixeldroid/app/utils/notificationsWorker/NotificationsWorker.kt
@@ -12,36 +12,41 @@ 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 org.pixeldroid.app.MainActivity
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import org.pixeldroid.app.main.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.*
+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.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
-class NotificationsWorker(
- context: Context,
- params: WorkerParameters
+@HiltWorker
+class NotificationsWorker @AssistedInject constructor(
+ @Assisted context: Context,
+ @Assisted params: WorkerParameters,
+ private val db: AppDatabase,
+ private val apiHolder: PixelfedAPIHolder
) : CoroutineWorker(context, params) {
- @Inject
- lateinit var db: AppDatabase
- @Inject
- lateinit var apiHolder: PixelfedAPIHolder
-
override suspend fun doWork(): Result {
val users: List = db.userDao().getAll()
diff --git a/app/src/main/res/drawable/add.xml b/app/src/main/res/drawable/add.xml
new file mode 100644
index 00000000..2ae27b84
--- /dev/null
+++ b/app/src/main/res/drawable/add.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/logout.xml b/app/src/main/res/drawable/logout.xml
new file mode 100644
index 00000000..bf421c22
--- /dev/null
+++ b/app/src/main/res/drawable/logout.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/person.xml b/app/src/main/res/drawable/person.xml
new file mode 100644
index 00000000..ddc83227
--- /dev/null
+++ b/app/src/main/res/drawable/person.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/settings.xml b/app/src/main/res/drawable/settings.xml
new file mode 100644
index 00000000..21228c71
--- /dev/null
+++ b/app/src/main/res/drawable/settings.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml
new file mode 100644
index 00000000..8affa9d7
--- /dev/null
+++ b/app/src/main/res/layout-land/activity_main.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout-land/post_fragment.xml b/app/src/main/res/layout-land/post_fragment.xml
new file mode 100644
index 00000000..802d68c6
--- /dev/null
+++ b/app/src/main/res/layout-land/post_fragment.xml
@@ -0,0 +1,258 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout-sw600dp-land/activity_main.xml b/app/src/main/res/layout-sw600dp-land/activity_main.xml
new file mode 100644
index 00000000..9764ff02
--- /dev/null
+++ b/app/src/main/res/layout-sw600dp-land/activity_main.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout-sw600dp/activity_main.xml b/app/src/main/res/layout-sw600dp/activity_main.xml
new file mode 100644
index 00000000..84fd0413
--- /dev/null
+++ b/app/src/main/res/layout-sw600dp/activity_main.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/account_list_item.xml b/app/src/main/res/layout/account_list_item.xml
new file mode 100644
index 00000000..04737f44
--- /dev/null
+++ b/app/src/main/res/layout/account_list_item.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml
index 9987fd18..9f8f4530 100644
--- a/app/src/main/res/layout/activity_login.xml
+++ b/app/src/main/res/layout/activity_login.xml
@@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".LoginActivity">
+ tools:context=".login.LoginActivity">
-
-
-
-
-
-
-
-
-
+ tools:context="org.pixeldroid.app.main.MainActivity">
+ app:menu="@menu/navigation_main" />
+
diff --git a/app/src/main/res/layout/post_fragment.xml b/app/src/main/res/layout/post_fragment.xml
index 204714ff..8db7c421 100644
--- a/app/src/main/res/layout/post_fragment.xml
+++ b/app/src/main/res/layout/post_fragment.xml
@@ -1,238 +1,238 @@
+ android:layout_marginBottom="5dp">
-
+
-
+
-
+
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ app:layout_constraintTop_toTopOf="parent" />
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu-sw600dp-land/navigation_main.xml b/app/src/main/res/menu-sw600dp-land/navigation_main.xml
new file mode 100644
index 00000000..e71b8f94
--- /dev/null
+++ b/app/src/main/res/menu-sw600dp-land/navigation_main.xml
@@ -0,0 +1,46 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/bottom_navigation_main.xml b/app/src/main/res/menu/navigation_main.xml
similarity index 100%
rename from app/src/main/res/menu/bottom_navigation_main.xml
rename to app/src/main/res/menu/navigation_main.xml
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 1d28a0dc..0c49fbe5 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -180,8 +180,8 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"
- "%d\nFollowing"
Edit
- Unable to save image
- Image successfully saved
+ Unable to save
+ Saved successfully
Could not get follow status
Failed to open edit page
Failed to open collection creation page
diff --git a/build.gradle b/build.gradle
index 5e54a8ee..4ee10a94 100644
--- a/build.gradle
+++ b/build.gradle
@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:8.3.1'
+ classpath 'com.android.tools.build:gradle:8.5.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
diff --git a/fastlane/metadata/android/en-US/changelogs/3404.txt b/fastlane/metadata/android/en-US/changelogs/3404.txt
new file mode 100644
index 00000000..dfdcff7d
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/3404.txt
@@ -0,0 +1,4 @@
+* Support for tablets, and rotation of all screens. There could still be some issues, please send us a message if there are
+* Bug fixes: notably, fix notifications which broke a couple releases back.
+* Translation updates
+* Update dependencies
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index df147285..465544df 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -2,6 +2,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=3e1af3ae886920c3ac87f7a91f816c0c7c436f276a6eefdb3da152100fef72ae
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists