diff --git a/app/build.gradle b/app/build.gradle index ce707c1b..e095cc94 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -74,13 +74,13 @@ dependencies { implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.preference:preference-ktx:1.1.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' - implementation 'androidx.navigation:navigation-fragment-ktx:2.3.2' - implementation 'androidx.navigation:navigation-ui-ktx:2.3.2' + implementation 'androidx.navigation:navigation-fragment-ktx:2.3.3' + implementation 'androidx.navigation:navigation-ui-ktx:2.3.3' implementation "androidx.browser:browser:1.3.0" implementation 'androidx.recyclerview:recyclerview:1.1.0' implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" - implementation 'androidx.navigation:navigation-fragment-ktx:2.3.2' - implementation 'androidx.navigation:navigation-ui-ktx:2.3.2' + implementation 'androidx.navigation:navigation-fragment-ktx:2.3.3' + implementation 'androidx.navigation:navigation-ui-ktx:2.3.3' implementation 'androidx.paging:paging-runtime-ktx:3.0.0-alpha12' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.2.0' @@ -90,16 +90,16 @@ dependencies { implementation 'androidx.gridlayout:gridlayout:1.0.0' // Use the most recent version of CameraX - def cameraX_version = '1.0.0-rc01' + def cameraX_version = '1.0.0-rc02' implementation "androidx.camera:camera-core:${cameraX_version}" implementation "androidx.camera:camera-camera2:${cameraX_version}" // CameraX Lifecycle library implementation "androidx.camera:camera-lifecycle:$cameraX_version" // CameraX View class - implementation 'androidx.camera:camera-view:1.0.0-alpha20' + implementation 'androidx.camera:camera-view:1.0.0-alpha21' - def room_version = "2.3.0-alpha04" + def room_version = "2.3.0-beta01" implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" implementation "androidx.room:room-ktx:$room_version" @@ -125,7 +125,7 @@ dependencies { implementation 'com.squareup.retrofit2:adapter-rxjava2:2.9.0' implementation 'io.reactivex.rxjava2:rxjava:2.2.20' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' - implementation 'com.github.connyduck:sparkbutton:4.0.0' + implementation 'com.github.connyduck:sparkbutton:4.1.0' implementation 'info.androidhive:imagefilters:1.0.7' diff --git a/app/src/main/java/com/h/pixeldroid/LoginActivity.kt b/app/src/main/java/com/h/pixeldroid/LoginActivity.kt index c81c8798..86aeb1c1 100644 --- a/app/src/main/java/com/h/pixeldroid/LoginActivity.kt +++ b/app/src/main/java/com/h/pixeldroid/LoginActivity.kt @@ -10,22 +10,15 @@ import android.view.View import android.view.inputmethod.InputMethodManager import androidx.lifecycle.lifecycleScope import com.h.pixeldroid.databinding.ActivityLoginBinding -import com.h.pixeldroid.databinding.ActivityPostCreationBinding -import com.h.pixeldroid.utils.BaseActivity +import com.h.pixeldroid.utils.* import com.h.pixeldroid.utils.api.PixelfedAPI import com.h.pixeldroid.utils.api.objects.* import com.h.pixeldroid.utils.db.addUser import com.h.pixeldroid.utils.db.storeInstance -import com.h.pixeldroid.utils.hasInternet -import com.h.pixeldroid.utils.normalizeDomain -import com.h.pixeldroid.utils.openUrl -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.async -import kotlinx.coroutines.launch -import kotlinx.coroutines.supervisorScope -import okhttp3.HttpUrl +import kotlinx.coroutines.* import retrofit2.HttpException import java.io.IOException +import java.lang.IllegalArgumentException /** Overview of the flow of the login process: (boxes are requests done in parallel, @@ -125,11 +118,7 @@ class LoginActivity : BaseActivity() { private fun registerAppToServer(normalizedDomain: String) { - try{ - HttpUrl.Builder().host(normalizedDomain.replace("https://", "")).scheme("https").build() - } catch (e: IllegalArgumentException) { - return failedRegistration(getString(R.string.invalid_domain)) - } + if(!validDomain(normalizedDomain)) failedRegistration(getString(R.string.invalid_domain)) hideKeyboard() loadingAnimation(true) @@ -138,7 +127,6 @@ class LoginActivity : BaseActivity() { lifecycleScope.launch { try { - supervisorScope { } val credentialsDeferred: Deferred = async { try { pixelfedAPI.registerApplication( @@ -157,7 +145,6 @@ class LoginActivity : BaseActivity() { val clientId = credentials?.client_id ?: return@launch failedRegistration() preferences.edit() - .putString("domain", normalizedDomain) .putString("clientID", clientId) .putString("clientSecret", credentials.client_secret) .apply() @@ -181,18 +168,39 @@ class LoginActivity : BaseActivity() { normalizedDomain: String, clientId: String, nodeInfoSchemaUrl: String - ) { - val nodeInfo = try { + ) = coroutineScope { + + val nodeInfo: NodeInfo = try { pixelfedAPI.nodeInfoSchema(nodeInfoSchemaUrl) } catch (exception: IOException) { - return failedRegistration(getString(R.string.instance_error)) + return@coroutineScope failedRegistration(getString(R.string.instance_error)) } catch (exception: HttpException) { - return failedRegistration(getString(R.string.instance_error)) + return@coroutineScope failedRegistration(getString(R.string.instance_error)) } + val domain: String = try { + if (nodeInfo.hasInstanceEndpointInfo()) { + storeInstance(db, nodeInfo) + nodeInfo.metadata?.config?.site?.url + } else { + val instance: Instance = try { + pixelfedAPI.instance() + } catch (exception: IOException) { + return@coroutineScope failedRegistration(getString(R.string.instance_error)) + } catch (exception: HttpException) { + return@coroutineScope failedRegistration(getString(R.string.instance_error)) + } + storeInstance(db, nodeInfo = null, instance = instance) + 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")) { - val builder = AlertDialog.Builder(this@LoginActivity) - builder.apply { + AlertDialog.Builder(this@LoginActivity).apply { setMessage(R.string.instance_not_pixelfed_warning) setPositiveButton(R.string.instance_not_pixelfed_continue) { _, _ -> promptOAuth(normalizedDomain, clientId) @@ -201,13 +209,18 @@ class LoginActivity : BaseActivity() { loadingAnimation(false) wipeSharedSettings() } - } - // Create the AlertDialog - builder.show() + }.show() + } else if (nodeInfo.metadata?.config?.features?.mobile_apis != true) { + AlertDialog.Builder(this@LoginActivity).apply { + setMessage(R.string.api_not_enabled_dialog) + setNegativeButton(android.R.string.ok) { _, _ -> + loadingAnimation(false) + wipeSharedSettings() + } + }.show() } else { promptOAuth(normalizedDomain, clientId) } - } @@ -238,9 +251,6 @@ class LoginActivity : BaseActivity() { lifecycleScope.launch { try { - val instanceDeferred = async { - pixelfedAPI.instance() - } val token = pixelfedAPI.obtainToken( clientId, clientSecret, "$oauthScheme://$PACKAGE_ID", SCOPE, code, "authorization_code" @@ -248,20 +258,12 @@ class LoginActivity : BaseActivity() { if (token.access_token == null) { return@launch failedRegistration(getString(R.string.token_error)) } - - val instance = instanceDeferred.await() - - if (instance.uri == null) { - return@launch failedRegistration(getString(R.string.instance_error)) - } - - storeInstance(db, instance) storeUser( token.access_token, token.refresh_token, clientId, clientSecret, - instance.uri + domain ) wipeSharedSettings() } catch (exception: IOException) { diff --git a/app/src/main/java/com/h/pixeldroid/MainActivity.kt b/app/src/main/java/com/h/pixeldroid/MainActivity.kt index fbfc9b2a..7bd05c53 100644 --- a/app/src/main/java/com/h/pixeldroid/MainActivity.kt +++ b/app/src/main/java/com/h/pixeldroid/MainActivity.kt @@ -61,15 +61,16 @@ class MainActivity : BaseActivity() { binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) - TraceDroidEmailSender.sendStackTraces("contact@pixeldroid.org", this) - //get the currently active user user = db.userDao().getActiveUser() //Check if we have logged in and gotten an access token if (user == null) { launchActivity(LoginActivity(), firstTime = true) + finish() } else { + TraceDroidEmailSender.sendStackTraces("contact@pixeldroid.org", this) + setupDrawer() val tabs: List<() -> Fragment> = listOf( diff --git a/app/src/main/java/com/h/pixeldroid/postCreation/PostCreationActivity.kt b/app/src/main/java/com/h/pixeldroid/postCreation/PostCreationActivity.kt index a3052b95..26267464 100644 --- a/app/src/main/java/com/h/pixeldroid/postCreation/PostCreationActivity.kt +++ b/app/src/main/java/com/h/pixeldroid/postCreation/PostCreationActivity.kt @@ -1,10 +1,8 @@ package com.h.pixeldroid.postCreation import android.app.Activity -import android.content.ContentResolver -import android.content.ContentValues -import android.content.Context -import android.content.Intent +import android.app.AlertDialog +import android.content.* import android.media.MediaScannerConnection import android.net.Uri import android.os.Build @@ -31,7 +29,7 @@ import com.h.pixeldroid.postCreation.photoEdit.PhotoEditActivity import com.h.pixeldroid.utils.BaseActivity import com.h.pixeldroid.utils.api.PixelfedAPI import com.h.pixeldroid.utils.api.objects.Attachment -import com.h.pixeldroid.utils.api.objects.Instance +import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable @@ -44,15 +42,18 @@ import java.io.OutputStream import java.text.SimpleDateFormat import java.util.* import kotlin.collections.ArrayList +import kotlin.math.ceil +import kotlin.properties.Delegates private const val TAG = "Post Creation Activity" private const val MORE_PICTURES_REQUEST_CODE = 0xffff data class PhotoData( var imageUri: Uri, + var size: Long, var uploadId: String? = null, var progress: Int? = null, - var imageDescription: String? = null + var imageDescription: String? = null, ) class PostCreationActivity : BaseActivity() { @@ -62,6 +63,7 @@ class PostCreationActivity : BaseActivity() { private var positionResult = 0 private var user: UserDatabaseEntity? = null + private lateinit var instance: InstanceDatabaseEntity private val photoData: ArrayList = ArrayList() @@ -72,29 +74,18 @@ class PostCreationActivity : BaseActivity() { binding = ActivityPostCreationBinding.inflate(layoutInflater) setContentView(binding.root) - // get image URIs - if(intent.clipData != null) { - val count = intent.clipData!!.itemCount - for (i in 0 until count) { - intent.clipData!!.getItemAt(i).uri.let { - photoData.add(PhotoData(it)) - } - } - } - user = db.userDao().getActiveUser() - val instances = db.instanceDao().getAll() + instance = user?.run { + db.instanceDao().getAll().first { instanceDatabaseEntity -> + instanceDatabaseEntity.uri.contains(instance_uri) + } + } ?: InstanceDatabaseEntity("", "") - binding.postTextInputLayout.counterMaxLength = if (user != null){ - val thisInstances = - instances.filter { instanceDatabaseEntity -> - instanceDatabaseEntity.uri.contains(user!!.instance_uri) - } - thisInstances.first().max_toot_chars - } else { - Instance.DEFAULT_MAX_TOOT_CHARS - } + binding.postTextInputLayout.counterMaxLength = instance.maxStatusChars + + // get image URIs + intent.clipData?.let { addPossibleImages(it) } accessToken = user?.accessToken.orEmpty() pixelfedAPI = apiHolder.api ?: apiHolder.setDomainToCurrentUser(db) @@ -102,15 +93,15 @@ class PostCreationActivity : BaseActivity() { val carousel: ImageCarousel = binding.carousel carousel.addData(photoData.map { CarouselItem(it.imageUri) }) carousel.layoutCarouselCallback = { - //TODO transition instead of at once if(it){ // Became a carousel - binding.toolbar3.visibility = VISIBLE + binding.toolbarPostCreation.visibility = VISIBLE } else { // Became a grid - binding.toolbar3.visibility = INVISIBLE + binding.toolbarPostCreation.visibility = INVISIBLE } } + carousel.maxEntries = instance.albumLimit carousel.addPhotoButtonCallback = { addPhoto(applicationContext) } @@ -154,6 +145,63 @@ class PostCreationActivity : BaseActivity() { carousel.currentPosition.takeIf { it != -1 }?.let { currentPosition -> photoData.removeAt(currentPosition) carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription) }) + binding.addPhotoButton.isEnabled = true + } + } + } + + /** + * Will add as many images as possible to [photoData], from the [clipData], and if + * ([photoData].size + [clipData].itemCount) > [albumLimit] 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. + */ + private fun addPossibleImages(clipData: ClipData){ + var count = clipData.itemCount + if(count + photoData.size > instance.albumLimit){ + AlertDialog.Builder(this).apply { + setMessage(getString(R.string.total_exceeds_album_limit).format(instance.albumLimit)) + setNegativeButton(android.R.string.ok) { _, _ -> } + }.show() + count = count.coerceAtMost(instance.albumLimit - photoData.size) + } + if (count + photoData.size >= instance.albumLimit) { + // Disable buttons to add more images + binding.addPhotoButton.isEnabled = false + } + for (i in 0 until count) { + clipData.getItemAt(i).uri.let { + val size: Long = + if (it.toString().startsWith("content")) { + contentResolver.query(it, 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, + * and display it. + */ + val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) + cursor.moveToFirst() + cursor.getLong(sizeIndex) + } ?: 0 + } else { + it.toFile().length() + } + val sizeInkBytes = ceil(size.toDouble() / 1000).toLong() + if(sizeInkBytes > instance.maxPhotoSize || sizeInkBytes > instance.maxVideoSize){ + val maxSize = when { + instance.maxPhotoSize != instance.maxVideoSize -> { + val type = contentResolver.getType(it) + if(type?.startsWith("video/") == true){ + instance.maxVideoSize + } else instance.maxPhotoSize + } + else -> instance.maxPhotoSize + } + AlertDialog.Builder(this).apply { + setMessage(getString(R.string.size_exceeds_instance_limit).format(photoData.size + 1, sizeInkBytes, maxSize)) + setNegativeButton(android.R.string.ok) { _, _ -> } + }.show() + } + photoData.add(PhotoData(imageUri = it, size = size)) } } } @@ -178,21 +226,21 @@ class PostCreationActivity : BaseActivity() { if(path.startsWith("file")) { MediaScannerConnection.scanFile( - this, - arrayOf(path.toUri().toFile().absolutePath), - null + this, + arrayOf(path.toUri().toFile().absolutePath), + null ) { path, uri -> if (uri == null) { Log.e( - "NEW IMAGE SCAN FAILED", - "Tried to scan $path, but it failed" + "NEW IMAGE SCAN FAILED", + "Tried to scan $path, but it failed" ) } } } Snackbar.make( - button, getString(R.string.save_image_success), - Snackbar.LENGTH_LONG + button, getString(R.string.save_image_success), + Snackbar.LENGTH_LONG ).show() } @@ -205,8 +253,8 @@ class PostCreationActivity : BaseActivity() { contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name) contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/png") contentValues.put( - MediaStore.MediaColumns.RELATIVE_PATH, - Environment.DIRECTORY_PICTURES + MediaStore.MediaColumns.RELATIVE_PATH, + Environment.DIRECTORY_PICTURES ) val imageUri: Uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)!! @@ -253,23 +301,7 @@ class PostCreationActivity : BaseActivity() { val imageUri = data.imageUri val imageInputStream = contentResolver.openInputStream(imageUri)!! - val size = - if (imageUri.toString().startsWith("content")) { - contentResolver.query(imageUri, 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, - * and display it. - */ - val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) - cursor.moveToFirst() - cursor.getLong(sizeIndex) - } ?: 0 - } else { - imageUri.toFile().length() - } - - val imagePart = ProgressRequestBody(imageInputStream, size) + val imagePart = ProgressRequestBody(imageInputStream, data.size) val requestBody = MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart("file", System.currentTimeMillis().toString(), imagePart) @@ -294,26 +326,33 @@ class PostCreationActivity : BaseActivity() { .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( - { attachment: Attachment -> - data.progress = 0 - data.uploadId = attachment.id!! - }, - { e -> - binding.uploadError.visibility = View.VISIBLE - e.printStackTrace() - postSub?.dispose() - sub.dispose() - }, - { - data.progress = 100 - if(photoData.all{it.progress == 100 && it.uploadId != null}){ - binding.uploadProgressBar.visibility = View.GONE - binding.uploadCompletedTextview.visibility = View.VISIBLE - post() + { attachment: Attachment -> + data.progress = 0 + data.uploadId = attachment.id!! + }, + { e: Throwable -> + binding.uploadError.visibility = View.VISIBLE + if(e is HttpException){ + binding.uploadErrorTextExplanation.text = + getString(R.string.upload_error).format(e.code()) + binding.uploadErrorTextExplanation.visibility= VISIBLE + } else { + binding.uploadErrorTextExplanation.visibility= View.GONE + } + e.printStackTrace() + postSub?.dispose() + sub.dispose() + }, + { + data.progress = 100 + if (photoData.all { it.progress == 100 && it.uploadId != null }) { + binding.uploadProgressBar.visibility = View.GONE + binding.uploadCompletedTextview.visibility = View.VISIBLE + post() + } + postSub?.dispose() + sub.dispose() } - postSub?.dispose() - sub.dispose() - } ) } } @@ -324,23 +363,23 @@ class PostCreationActivity : BaseActivity() { lifecycleScope.launchWhenCreated { try { pixelfedAPI.postStatus( - authorization = "Bearer $accessToken", - statusText = description, - media_ids = photoData.mapNotNull { it.uploadId }.toList() + authorization = "Bearer $accessToken", + statusText = description, + media_ids = photoData.mapNotNull { it.uploadId }.toList() ) - Toast.makeText(applicationContext,getString(R.string.upload_post_success), - Toast.LENGTH_SHORT).show() + Toast.makeText(applicationContext, getString(R.string.upload_post_success), + Toast.LENGTH_SHORT).show() val intent = Intent(this@PostCreationActivity, MainActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK startActivity(intent) } catch (exception: IOException) { - Toast.makeText(applicationContext,getString(R.string.upload_post_error), - Toast.LENGTH_SHORT).show() + Toast.makeText(applicationContext, getString(R.string.upload_post_error), + Toast.LENGTH_SHORT).show() Log.e(TAG, exception.toString()) enableButton(true) } catch (exception: HttpException) { - Toast.makeText(applicationContext,getString(R.string.upload_post_failed), - Toast.LENGTH_SHORT).show() + Toast.makeText(applicationContext, getString(R.string.upload_post_failed), + Toast.LENGTH_SHORT).show() Log.e(TAG, exception.response().toString() + exception.message().toString()) enableButton(true) } @@ -382,12 +421,10 @@ class PostCreationActivity : BaseActivity() { Toast.makeText(applicationContext, "Error while editing", Toast.LENGTH_SHORT).show() } } else if (requestCode == MORE_PICTURES_REQUEST_CODE) { - if (resultCode == Activity.RESULT_OK && data?.clipData != null) { - val count = data.clipData!!.itemCount - for (i in 0 until count) { - val imageUri: Uri = data.clipData!!.getItemAt(i).uri - photoData.add(PhotoData(imageUri)) + if (resultCode == Activity.RESULT_OK && data?.clipData != null) { + data.clipData?.let { + addPossibleImages(it) } binding.carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription) }) diff --git a/app/src/main/java/com/h/pixeldroid/postCreation/carousel/CarouselAdapter.kt b/app/src/main/java/com/h/pixeldroid/postCreation/carousel/CarouselAdapter.kt index b2ecb451..759343fc 100644 --- a/app/src/main/java/com/h/pixeldroid/postCreation/carousel/CarouselAdapter.kt +++ b/app/src/main/java/com/h/pixeldroid/postCreation/carousel/CarouselAdapter.kt @@ -13,18 +13,17 @@ import com.h.pixeldroid.R class CarouselAdapter( - @LayoutRes private val itemLayout: Int, - @IdRes private val imageViewId: Int, - var listener: OnItemClickListener? = null, - private val imageScaleType: ImageView.ScaleType, - private val imagePlaceholder: Drawable?, - private val carousel: Boolean + @LayoutRes private val itemLayout: Int, + @IdRes private val imageViewId: Int, + var listener: OnItemClickListener? = null, + private val imageScaleType: ImageView.ScaleType, + private val imagePlaceholder: Drawable?, + private val carousel: Boolean, + var maxEntries: Int?, ) : RecyclerView.Adapter() { private val dataList: MutableList = mutableListOf() - - class MyViewHolder(itemView: View, imageViewId: Int) : RecyclerView.ViewHolder(itemView) { var img: ImageView = itemView.findViewById(imageViewId) } @@ -51,6 +50,7 @@ class CarouselAdapter( override fun getItemCount(): Int { return if(carousel) dataList.size + else if (maxEntries != null && dataList.size >= maxEntries!!) maxEntries!! else dataList.size + 1 } diff --git a/app/src/main/java/com/h/pixeldroid/postCreation/carousel/ImageCarousel.kt b/app/src/main/java/com/h/pixeldroid/postCreation/carousel/ImageCarousel.kt index d631a4bb..88a7913b 100644 --- a/app/src/main/java/com/h/pixeldroid/postCreation/carousel/ImageCarousel.kt +++ b/app/src/main/java/com/h/pixeldroid/postCreation/carousel/ImageCarousel.kt @@ -318,6 +318,12 @@ class ImageCarousel( } + var maxEntries: Int? = null + set(value){ + field = value + adapter?.maxEntries = value + } + init { @@ -419,12 +425,13 @@ class ImageCarousel( private fun initAdapter() { adapter = CarouselAdapter( - itemLayout = itemLayout, - imageViewId = imageViewId, - listener = onItemClickListener, - imageScaleType = imageScaleType, - imagePlaceholder = imagePlaceholder, - carousel = layoutCarousel + itemLayout = itemLayout, + imageViewId = imageViewId, + listener = onItemClickListener, + imageScaleType = imageScaleType, + imagePlaceholder = imagePlaceholder, + carousel = layoutCarousel, + maxEntries = maxEntries ) recyclerView.adapter = adapter diff --git a/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/accountLists/FollowersPagingSource.kt b/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/accountLists/FollowersPagingSource.kt index 204b3ae3..d219da09 100644 --- a/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/accountLists/FollowersPagingSource.kt +++ b/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/accountLists/FollowersPagingSource.kt @@ -1,6 +1,7 @@ package com.h.pixeldroid.posts.feeds.uncachedFeeds.accountLists import androidx.paging.PagingSource +import androidx.paging.PagingState import com.h.pixeldroid.utils.api.PixelfedAPI import com.h.pixeldroid.utils.api.objects.Account import retrofit2.HttpException diff --git a/app/src/main/java/com/h/pixeldroid/utils/Utils.kt b/app/src/main/java/com/h/pixeldroid/utils/Utils.kt index 03a3b359..cea99310 100644 --- a/app/src/main/java/com/h/pixeldroid/utils/Utils.kt +++ b/app/src/main/java/com/h/pixeldroid/utils/Utils.kt @@ -14,6 +14,7 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import com.h.pixeldroid.R +import okhttp3.HttpUrl import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty @@ -22,6 +23,21 @@ fun hasInternet(context: Context): Boolean { return cm.activeNetwork != null } +/** + * Check if domain is valid or not + */ +fun validDomain(domain: String?): Boolean { + domain?.apply { + try { + HttpUrl.Builder().host(replace("https://", "")).scheme("https").build() + } catch (e: IllegalArgumentException) { + return false + } + } ?: return false + + return true +} + fun normalizeDomain(domain: String): String { return "https://" + domain .replace("http://", "") diff --git a/app/src/main/java/com/h/pixeldroid/utils/api/objects/Instance.kt b/app/src/main/java/com/h/pixeldroid/utils/api/objects/Instance.kt index b8031b35..bfac27d0 100644 --- a/app/src/main/java/com/h/pixeldroid/utils/api/objects/Instance.kt +++ b/app/src/main/java/com/h/pixeldroid/utils/api/objects/Instance.kt @@ -1,5 +1,7 @@ package com.h.pixeldroid.utils.api.objects +import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity.Companion.DEFAULT_MAX_TOOT_CHARS + data class Instance ( val description: String?, val email: String?, @@ -9,8 +11,4 @@ data class Instance ( val title: String?, val uri: String?, val version: String? -) { - companion object { - const val DEFAULT_MAX_TOOT_CHARS = 500 - } -} \ No newline at end of file +) \ No newline at end of file diff --git a/app/src/main/java/com/h/pixeldroid/utils/api/objects/NodeInfo.kt b/app/src/main/java/com/h/pixeldroid/utils/api/objects/NodeInfo.kt index 32ff27e2..e302c88f 100644 --- a/app/src/main/java/com/h/pixeldroid/utils/api/objects/NodeInfo.kt +++ b/app/src/main/java/com/h/pixeldroid/utils/api/objects/NodeInfo.kt @@ -1,5 +1,7 @@ package com.h.pixeldroid.utils.api.objects +import com.h.pixeldroid.utils.validDomain + /* See https://nodeinfo.diaspora.software/schema.html and https://pixelfed.social/api/nodeinfo/2.0.json A lot of attributes we don't need are omitted, if in the future they are needed we @@ -11,8 +13,21 @@ data class NodeInfo ( val software: Software?, val protocols: List?, val openRegistrations: Boolean?, - val metadata: PixelfedMetadata? + val metadata: PixelfedMetadata?, ){ + /** + * Check if this NodeInfo has the fields we need or if we also need to look into the + * /api/v1/instance endpoint + * This only checks for values that might be in the /api/v1/instance endpoint. + */ + fun hasInstanceEndpointInfo(): Boolean { + return validDomain(metadata?.config?.site?.url) + && !metadata?.config?.site?.name.isNullOrBlank() + && metadata?.config?.uploader?.max_caption_length?.toIntOrNull() != null + } + + + data class Software( val name: String?, val version: String? @@ -31,7 +46,8 @@ data class NodeInfo ( val open_registration: Boolean?, val uploader: Uploader?, val activitypub: ActivityPub?, - val features: Features? + val features: Features?, + val site: Site? ){ data class Uploader( val max_photo_size: String?, @@ -55,6 +71,13 @@ data class NodeInfo ( val stories: Boolean?, val video: Boolean? ) + + data class Site( + val name: String?, + val domain: String?, + val url: String?, + val description: String? + ) } } diff --git a/app/src/main/java/com/h/pixeldroid/utils/db/AppDatabase.kt b/app/src/main/java/com/h/pixeldroid/utils/db/AppDatabase.kt index 876f6bc2..e81e1d1d 100644 --- a/app/src/main/java/com/h/pixeldroid/utils/db/AppDatabase.kt +++ b/app/src/main/java/com/h/pixeldroid/utils/db/AppDatabase.kt @@ -20,7 +20,7 @@ import com.h.pixeldroid.utils.api.objects.Notification PublicFeedStatusDatabaseEntity::class, Notification::class ], - version = 2 + version = 3 ) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { diff --git a/app/src/main/java/com/h/pixeldroid/utils/db/DBUtils.kt b/app/src/main/java/com/h/pixeldroid/utils/db/DBUtils.kt index 506dc799..9193bf07 100644 --- a/app/src/main/java/com/h/pixeldroid/utils/db/DBUtils.kt +++ b/app/src/main/java/com/h/pixeldroid/utils/db/DBUtils.kt @@ -4,23 +4,20 @@ import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity import com.h.pixeldroid.utils.api.objects.Account import com.h.pixeldroid.utils.api.objects.Instance +import com.h.pixeldroid.utils.api.objects.NodeInfo +import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity.Companion.DEFAULT_ALBUM_LIMIT +import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity.Companion.DEFAULT_MAX_PHOTO_SIZE +import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity.Companion.DEFAULT_MAX_TOOT_CHARS +import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity.Companion.DEFAULT_MAX_VIDEO_SIZE import com.h.pixeldroid.utils.normalizeDomain - -private fun normalizeOrNot(uri: String): String{ - return if(uri.startsWith("http://localhost")){ - uri - } else { - normalizeDomain(uri) - } -} +import java.lang.IllegalArgumentException fun addUser(db: AppDatabase, account: Account, instance_uri: String, activeUser: Boolean = true, accessToken: String, refreshToken: String?, clientId: String, clientSecret: String) { db.userDao().insertUser( UserDatabaseEntity( user_id = account.id!!, - //make sure not to normalize to https when localhost, to allow testing - instance_uri = normalizeOrNot(instance_uri), + instance_uri = normalizeDomain(instance_uri), username = account.username!!, display_name = account.getDisplayName(), avatar_static = account.avatar_static.orEmpty(), @@ -33,14 +30,24 @@ fun addUser(db: AppDatabase, account: Account, instance_uri: String, activeUser: ) } -fun storeInstance(db: AppDatabase, instance: Instance) { - val maxTootChars = instance.max_toot_chars?.toInt() ?: Instance.DEFAULT_MAX_TOOT_CHARS - val dbInstance = InstanceDatabaseEntity( - //make sure not to normalize to https when localhost, to allow testing - uri = normalizeOrNot(instance.uri.orEmpty()), - title = instance.title.orEmpty(), - max_toot_chars = maxTootChars, - thumbnail = instance.thumbnail.orEmpty() - ) +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, + //Pixelfed doesn't distinguish between max photo and video size + maxVideoSize = metadata.config.uploader.max_photo_size?.toIntOrNull() ?: DEFAULT_MAX_VIDEO_SIZE, + albumLimit = metadata.config.uploader.album_limit?.toIntOrNull() ?: DEFAULT_ALBUM_LIMIT + ) + } ?: instance?.run { + InstanceDatabaseEntity( + 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") + db.instanceDao().insertInstance(dbInstance) } \ No newline at end of file diff --git a/app/src/main/java/com/h/pixeldroid/utils/db/entities/InstanceDatabaseEntity.kt b/app/src/main/java/com/h/pixeldroid/utils/db/entities/InstanceDatabaseEntity.kt index 4b647b2a..2906dcb1 100644 --- a/app/src/main/java/com/h/pixeldroid/utils/db/entities/InstanceDatabaseEntity.kt +++ b/app/src/main/java/com/h/pixeldroid/utils/db/entities/InstanceDatabaseEntity.kt @@ -6,8 +6,23 @@ import com.h.pixeldroid.utils.api.objects.Instance @Entity(tableName = "instances") data class InstanceDatabaseEntity ( - @PrimaryKey var uri: String, - var title: String = "", - var max_toot_chars: Int = Instance.DEFAULT_MAX_TOOT_CHARS, - var thumbnail: String = "" -) \ No newline at end of file + @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, +) { + 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 + + const val DEFAULT_MAX_PHOTO_SIZE = 8000 + const val DEFAULT_MAX_VIDEO_SIZE = 40000 + const val DEFAULT_ALBUM_LIMIT = 4 + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/add_photo_alternate_gray_30dp.xml b/app/src/main/res/drawable/add_photo_alternate_gray_30dp.xml new file mode 100644 index 00000000..796fe501 --- /dev/null +++ b/app/src/main/res/drawable/add_photo_alternate_gray_30dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/add_photo_button.xml b/app/src/main/res/drawable/add_photo_button.xml new file mode 100644 index 00000000..be312a2f --- /dev/null +++ b/app/src/main/res/drawable/add_photo_button.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/layout/activity_post_creation.xml b/app/src/main/res/layout/activity_post_creation.xml index 362bad7b..cdbb9ecc 100644 --- a/app/src/main/res/layout/activity_post_creation.xml +++ b/app/src/main/res/layout/activity_post_creation.xml @@ -8,14 +8,15 @@ + app:layout_constraintTop_toTopOf="parent" + tools:visibility="visible"> + +