diff --git a/app/build.gradle b/app/build.gradle index be0a08503..ed6ba2d6f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -102,12 +102,12 @@ project.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { } } -ext.lifecycleVersion = "2.1.0" -ext.roomVersion = '2.2.3' -ext.retrofitVersion = '2.6.0' +ext.lifecycleVersion = "2.2.0" +ext.roomVersion = '2.2.4' +ext.retrofitVersion = '2.7.1' ext.okhttpVersion = '4.3.1' ext.glideVersion = '4.10.0' -ext.daggerVersion = '2.25.3' +ext.daggerVersion = '2.26' repositories { maven { @@ -119,9 +119,9 @@ repositories { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation "androidx.core:core-ktx:1.2.0-rc01" + implementation "androidx.core:core-ktx:1.2.0" implementation "androidx.appcompat:appcompat:1.2.0-alpha02" - implementation "androidx.fragment:fragment-ktx:1.1.0" + implementation "androidx.fragment:fragment-ktx:1.2.2" implementation "androidx.browser:browser:1.2.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" implementation "androidx.recyclerview:recyclerview:1.1.0" @@ -131,8 +131,9 @@ dependencies { implementation "androidx.sharetarget:sharetarget:1.0.0-rc01" implementation "androidx.emoji:emoji:1.0.0" implementation "androidx.emoji:emoji-appcompat:1.0.0" - implementation "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion" - implementation "androidx.lifecycle:lifecycle-reactivestreams:$lifecycleVersion" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion" + implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycleVersion" implementation "androidx.constraintlayout:constraintlayout:1.1.3" implementation "androidx.paging:paging-runtime-ktx:2.1.1" implementation "androidx.viewpager2:viewpager2:1.0.0" @@ -140,7 +141,7 @@ dependencies { implementation "androidx.room:room-rxjava2:$roomVersion" kapt "androidx.room:room-compiler:$roomVersion" - implementation "com.google.android.material:material:1.1.0-rc01" + implementation "com.google.android.material:material:1.1.0" implementation "com.squareup.retrofit2:retrofit:$retrofitVersion" implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion" diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt index 2a7d4e6cb..ecd334bc8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt @@ -27,6 +27,7 @@ import android.view.Menu import android.view.MenuItem import android.view.View import android.view.ViewGroup +import androidx.activity.viewModels import androidx.annotation.ColorInt import androidx.annotation.Px import androidx.appcompat.app.AlertDialog @@ -34,7 +35,6 @@ import androidx.core.app.ActivityOptionsCompat import androidx.core.content.ContextCompat import androidx.emoji.text.EmojiCompat import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders import androidx.preference.PreferenceManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.viewpager2.widget.MarginPageTransformer @@ -76,7 +76,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI @Inject lateinit var viewModelFactory: ViewModelFactory - private lateinit var viewModel: AccountViewModel + private val viewModel: AccountViewModel by viewModels { viewModelFactory } private val accountFieldAdapter = AccountFieldAdapter(this) @@ -116,9 +116,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI loadResources() makeNotificationBarTransparent() setContentView(R.layout.activity_account) - - viewModel = ViewModelProviders.of(this, viewModelFactory)[AccountViewModel::class.java] - + // Obtain information to fill out the profile. viewModel.setAccountInfo(intent.getStringExtra(KEY_ACCOUNT_ID)!!) diff --git a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java index d1718bea7..363872684 100644 --- a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java @@ -92,7 +92,7 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab @Override protected void attachBaseContext(Context base) { - super.attachBaseContext(TuskyApplication.localeManager.setLocale(base)); + super.attachBaseContext(TuskyApplication.getLocaleManager().setLocale(base)); } protected boolean requiresLogin() { diff --git a/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt b/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt index 416087a9e..02fde50db 100644 --- a/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt @@ -178,11 +178,13 @@ abstract class BottomSheetActivity : BaseActivity() { // https://mastodon.foo.bar/@User // https://mastodon.foo.bar/@User/43456787654678 // https://pleroma.foo.bar/users/User -// https://pleroma.foo.bar/users/43456787654678 -// https://pleroma.foo.bar/notice/43456787654678 +// https://pleroma.foo.bar/users/9qTHT2ANWUdXzENqC0 +// https://pleroma.foo.bar/notice/9sBHWIlwwGZi5QGlHc // https://pleroma.foo.bar/objects/d4643c42-3ae0-4b73-b8b0-c725f5819207 +// https://friendica.foo.bar/profile/user +// https://friendica.foo.bar/display/d4643c42-3ae0-4b73-b8b0-c725f5819207 +// https://misskey.foo.bar/notes/83w6r388br (always lowercase) // https://mastodon.foo.bar/users/User/statuses/000000000000000000 -// https://new.misskey.foo.bar/notes/012789abyz // username@example.com fun looksLikeMastodonUrl(urlString: String): Boolean { val uri: URI @@ -200,14 +202,15 @@ fun looksLikeMastodonUrl(urlString: String): Boolean { val path = uri.path return path.matches("^/@[^/]+$".toRegex()) || - path.matches("^/users/[^/]+$".toRegex()) || path.matches("^/@[^/]+/\\d+$".toRegex()) || - path.matches("^/notice/\\d+$".toRegex()) || + path.matches("^/users/\\w+$".toRegex()) || + path.matches("^/notice/[a-zA-Z0-9]+$".toRegex()) || path.matches("^/objects/[-a-f0-9]+$".toRegex()) || - path.matches("^/users/[^/]+/statuses/[0-9]+$".toRegex()) || path.matches("^/notes/[a-z0-9]+$".toRegex()) || + path.matches("^/display/[-a-f0-9]+$".toRegex()) || + path.matches("^/profile/\\w+$".toRegex()) || + path.matches("^/users/[^/]+/statuses/[0-9]+$".toRegex()) || path.matches("^[^@]+@[^@]+$".toRegex()) - } enum class PostLookupFallbackBehavior { diff --git a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt index 06f9c9b51..58da33df9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt @@ -19,7 +19,6 @@ import android.Manifest import android.app.Activity import androidx.lifecycle.LiveData import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders import android.content.Intent import android.content.pm.PackageManager import android.graphics.Bitmap @@ -34,6 +33,7 @@ import android.view.Menu import android.view.MenuItem import android.view.View import android.widget.ImageView +import androidx.activity.viewModels import com.bumptech.glide.Glide import com.bumptech.glide.load.resource.bitmap.FitCenter import com.bumptech.glide.load.resource.bitmap.RoundedCorners @@ -69,7 +69,7 @@ class EditProfileActivity : BaseActivity(), Injectable { @Inject lateinit var viewModelFactory: ViewModelFactory - private lateinit var viewModel: EditProfileViewModel + private val viewModel: EditProfileViewModel by viewModels { viewModelFactory } private var currentlyPicking: PickType = PickType.NOTHING @@ -90,8 +90,6 @@ class EditProfileActivity : BaseActivity(), Injectable { setContentView(R.layout.activity_edit_profile) - viewModel = ViewModelProviders.of(this, viewModelFactory)[EditProfileViewModel::class.java] - setSupportActionBar(toolbar) supportActionBar?.run { setTitle(R.string.title_edit_profile) diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java deleted file mode 100644 index 6bf824613..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java +++ /dev/null @@ -1,158 +0,0 @@ -/* Copyright 2017 Andrew Dawson - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky; - -import android.app.Application; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.res.Configuration; - -import androidx.emoji.text.EmojiCompat; -import androidx.preference.PreferenceManager; -import androidx.room.Room; - -import com.evernote.android.job.JobManager; -import com.keylesspalace.tusky.db.AccountManager; -import com.keylesspalace.tusky.db.AppDatabase; -import com.keylesspalace.tusky.di.AppInjector; -import com.keylesspalace.tusky.util.EmojiCompatFont; -import com.keylesspalace.tusky.util.LocaleManager; -import com.keylesspalace.tusky.util.NotificationPullJobCreator; -import com.keylesspalace.tusky.util.ThemeUtils; -import com.uber.autodispose.AutoDisposePlugins; - -import org.conscrypt.Conscrypt; - -import java.security.Security; - -import javax.inject.Inject; - -import dagger.android.AndroidInjector; -import dagger.android.DispatchingAndroidInjector; -import dagger.android.HasAndroidInjector; - -public class TuskyApplication extends Application implements HasAndroidInjector { - @Inject - DispatchingAndroidInjector androidInjector; - - @Inject - NotificationPullJobCreator notificationPullJobCreator; - - private AppDatabase appDatabase; - private AccountManager accountManager; - - private ServiceLocator serviceLocator; - - public static LocaleManager localeManager; - - @Override - public void onCreate() { - super.onCreate(); - - initSecurityProvider(); - - appDatabase = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "tuskyDB") - .allowMainThreadQueries() - .addMigrations(AppDatabase.MIGRATION_2_3, AppDatabase.MIGRATION_3_4, AppDatabase.MIGRATION_4_5, - AppDatabase.MIGRATION_5_6, AppDatabase.MIGRATION_6_7, AppDatabase.MIGRATION_7_8, - AppDatabase.MIGRATION_8_9, AppDatabase.MIGRATION_9_10, AppDatabase.MIGRATION_10_11, - AppDatabase.MIGRATION_11_12, AppDatabase.MIGRATION_12_13, AppDatabase.MIGRATION_10_13, - AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16, - AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19, - AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21) - .build(); - accountManager = new AccountManager(appDatabase); - serviceLocator = new ServiceLocator() { - @Override - public T get(Class clazz) { - if (clazz.equals(AccountManager.class)) { - //noinspection unchecked - return (T) accountManager; - } else if (clazz.equals(AppDatabase.class)) { - //noinspection unchecked - return (T) appDatabase; - } else { - throw new IllegalArgumentException("Unknown service " + clazz); - } - } - }; - - AutoDisposePlugins.setHideProxies(false); - - initAppInjector(); - initEmojiCompat(); - initNightMode(); - - JobManager.create(this).addJobCreator(notificationPullJobCreator); - - } - - protected void initSecurityProvider() { - Security.insertProviderAt(Conscrypt.newProvider(), 1); - } - - @Override - protected void attachBaseContext(Context base) { - localeManager = new LocaleManager(base); - super.attachBaseContext(localeManager.setLocale(base)); - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - localeManager.setLocale(this); - } - - /** - * This method will load the EmojiCompat font which has been selected. - * If this font does not work or if the user hasn't selected one (yet), it will use a - * fallback solution instead which won't make any visible difference to using no EmojiCompat at all. - */ - private void initEmojiCompat() { - int emojiSelection = PreferenceManager - .getDefaultSharedPreferences(getApplicationContext()) - .getInt(EmojiPreference.FONT_PREFERENCE, 0); - EmojiCompatFont font = EmojiCompatFont.byId(emojiSelection); - // FileEmojiCompat will handle any non-existing font and provide a fallback solution. - EmojiCompat.Config config = font.getConfig(getApplicationContext()) - // The user probably wants to get a consistent experience - .setReplaceAll(true); - EmojiCompat.init(config); - } - - protected void initAppInjector() { - AppInjector.INSTANCE.init(this); - } - - protected void initNightMode() { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - String theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT); - ThemeUtils.setAppNightMode(theme); - } - - public ServiceLocator getServiceLocator() { - return serviceLocator; - } - - @Override - public AndroidInjector androidInjector() { - return androidInjector; - } - - public interface ServiceLocator { - T get(Class clazz); - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt new file mode 100644 index 000000000..a86745a49 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt @@ -0,0 +1,85 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky + +import android.app.Application +import android.content.Context +import android.content.res.Configuration +import androidx.emoji.text.EmojiCompat +import androidx.preference.PreferenceManager +import com.evernote.android.job.JobManager +import com.keylesspalace.tusky.di.AppInjector +import com.keylesspalace.tusky.util.EmojiCompatFont +import com.keylesspalace.tusky.util.LocaleManager +import com.keylesspalace.tusky.util.NotificationPullJobCreator +import com.keylesspalace.tusky.util.ThemeUtils +import com.uber.autodispose.AutoDisposePlugins +import dagger.android.DispatchingAndroidInjector +import dagger.android.HasAndroidInjector +import org.conscrypt.Conscrypt +import java.security.Security +import javax.inject.Inject + +class TuskyApplication : Application(), HasAndroidInjector { + + @Inject + lateinit var androidInjector: DispatchingAndroidInjector + @Inject + lateinit var notificationPullJobCreator: NotificationPullJobCreator + + override fun onCreate() { + + super.onCreate() + + Security.insertProviderAt(Conscrypt.newProvider(), 1) + + AutoDisposePlugins.setHideProxies(false) // a small performance optimization + + AppInjector.init(this) + + val preferences = PreferenceManager.getDefaultSharedPreferences(this) + + // init the custom emoji fonts + val emojiSelection = preferences.getInt(EmojiPreference.FONT_PREFERENCE, 0) + val emojiConfig = EmojiCompatFont.byId(emojiSelection) + .getConfig(this) + .setReplaceAll(true) + EmojiCompat.init(emojiConfig) + + // init night mode + val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT) + ThemeUtils.setAppNightMode(theme) + + JobManager.create(this).addJobCreator(notificationPullJobCreator) + } + + override fun attachBaseContext(base: Context) { + localeManager = LocaleManager(base) + super.attachBaseContext(localeManager.setLocale(base)) + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + localeManager.setLocale(this) + } + + override fun androidInjector() = androidInjector + + companion object { + @JvmStatic + lateinit var localeManager: LocaleManager + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt index ae158aab6..ea76611db 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt @@ -39,6 +39,7 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.* +import androidx.activity.viewModels import androidx.annotation.ColorInt import androidx.annotation.StringRes import androidx.annotation.VisibleForTesting @@ -52,7 +53,6 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.widget.doAfterTextChanged import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders import androidx.preference.PreferenceManager import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager @@ -70,6 +70,7 @@ import com.keylesspalace.tusky.appstore.PreferenceChangedEvent import com.keylesspalace.tusky.components.compose.dialog.makeCaptionDialog import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener +import com.keylesspalace.tusky.components.compose.view.ComposeScheduleView import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory @@ -110,14 +111,12 @@ class ComposeActivity : BaseActivity(), // this only exists when a status is trying to be sent, but uploads are still occurring private var finishingUploadDialog: ProgressDialog? = null - private var currentInputContentInfo: InputContentInfoCompat? = null - private var currentFlags: Int = 0 private var photoUploadUri: Uri? = null @VisibleForTesting var maximumTootCharacters = DEFAULT_CHARACTER_LIMIT private var composeOptions: ComposeOptions? = null - private lateinit var viewModel: ComposeViewModel + private val viewModel: ComposeViewModel by viewModels { viewModelFactory } private var mediaCount = 0 @@ -149,11 +148,11 @@ class ComposeActivity : BaseActivity(), composeMediaPreviewBar.adapter = mediaAdapter composeMediaPreviewBar.itemAnimator = null - viewModel = ViewModelProviders.of(this, viewModelFactory)[ComposeViewModel::class.java] - subscribeToUpdates(mediaAdapter) setupButtons() + photoUploadUri = savedInstanceState?.getParcelable(PHOTO_UPLOAD_URI_KEY) + /* If the composer is started up as a reply to another post, override the "starting" state * based on what the intent from the reply request passes. */ if (intent != null) { @@ -544,14 +543,7 @@ class ComposeActivity : BaseActivity(), } override fun onSaveInstanceState(outState: Bundle) { - if (currentInputContentInfo != null) { - outState.putParcelable("commitContentInputContentInfo", - currentInputContentInfo!!.unwrap() as Parcelable?) - outState.putInt("commitContentFlags", currentFlags) - } - currentInputContentInfo = null - currentFlags = 0 - outState.putParcelable("photoUploadUri", photoUploadUri) + outState.putParcelable(PHOTO_UPLOAD_URI_KEY, photoUploadUri) super.onSaveInstanceState(outState) } @@ -777,47 +769,42 @@ class ComposeActivity : BaseActivity(), updateVisibleCharactersLeft() } + private fun verifyScheduledTime(): Boolean { + return composeScheduleView.verifyScheduledTime(composeScheduleView.getDateTime(viewModel.scheduledAt.value)) + } + private fun onSendClicked() { - enableButtons(false) - sendStatus() + if (verifyScheduledTime()) { + sendStatus() + } else { + showScheduleView() + } } /** This is for the fancy keyboards which can insert images and stuff. */ - override fun onCommitContent(inputContentInfo: InputContentInfoCompat, flags: Int, opts: Bundle): Boolean { - try { - currentInputContentInfo?.releasePermission() - } catch (e: Exception) { - Log.e(TAG, "InputContentInfoCompat#releasePermission() failed." + e.message) - } finally { - currentInputContentInfo = null - } - + override fun onCommitContent(inputContentInfo: InputContentInfoCompat, flags: Int, opts: Bundle?): Boolean { // Verify the returned content's type is of the correct MIME type val supported = inputContentInfo.description.hasMimeType("image/*") - return supported && onCommitContentInternal(inputContentInfo, flags) - } - - private fun onCommitContentInternal(inputContentInfo: InputContentInfoCompat, flags: Int): Boolean { - if (flags and InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION != 0) { - try { - inputContentInfo.requestPermission() - } catch (e: Exception) { - Log.e(TAG, "InputContentInfoCompat#requestPermission() failed." + e.message) - return false + if(supported) { + val lacksPermission = (flags and InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0 + if(lacksPermission) { + try { + inputContentInfo.requestPermission() + } catch (e: Exception) { + Log.e(TAG, "InputContentInfoCompat#requestPermission() failed." + e.message) + return false + } } + pickMedia(inputContentInfo.contentUri, inputContentInfo) + return true } - // Determine the file size before putting handing it off to be put in the queue. - pickMedia(inputContentInfo.contentUri) - - currentInputContentInfo = inputContentInfo - currentFlags = flags - - return true + return false } private fun sendStatus() { + enableButtons(false) var contentText = composeEditField.text.toString() var spoilerText = "" if (viewModel.showContentWarning.value!!) { @@ -927,9 +914,12 @@ class ComposeActivity : BaseActivity(), } } - private fun pickMedia(uri: Uri) { + private fun pickMedia(uri: Uri, contentInfoCompat: InputContentInfoCompat? = null) { withLifecycleContext { viewModel.pickMedia(uri).observe { exceptionOrItem -> + + contentInfoCompat?.releasePermission() + exceptionOrItem.asLeftOrNull()?.let { val errorId = when (it) { is VideoSizeException -> { @@ -1069,7 +1059,11 @@ class ComposeActivity : BaseActivity(), override fun onTimeSet(view: TimePicker, hourOfDay: Int, minute: Int) { composeScheduleView.onTimeSet(hourOfDay, minute) viewModel.updateScheduledAt(composeScheduleView.time) - scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN + if (verifyScheduledTime()) { + scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } else { + showScheduleView() + } } private fun resetSchedule() { @@ -1107,6 +1101,7 @@ class ComposeActivity : BaseActivity(), private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1 private const val COMPOSE_OPTIONS_EXTRA = "COMPOSE_OPTIONS" + private const val PHOTO_UPLOAD_URI_KEY = "PHOTO_UPLOAD_URI" // Mastodon only counts URLs as this long in terms of status character limits @VisibleForTesting diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeScheduleView.java b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeScheduleView.java index af10b2776..0deb20be7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeScheduleView.java +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeScheduleView.java @@ -22,6 +22,8 @@ import android.util.AttributeSet; import android.widget.Button; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.constraintlayout.widget.ConstraintLayout; @@ -48,8 +50,10 @@ public class ComposeScheduleView extends ConstraintLayout { private Button resetScheduleButton; private TextView scheduledDateTimeView; + private TextView invalidScheduleWarningView; private Calendar scheduleDateTime; + public static int MINIMUM_SCHEDULED_SECONDS = 330; // Minimum is 5 minutes, pad 30 seconds for posting public ComposeScheduleView(Context context) { super(context); @@ -76,8 +80,10 @@ public class ComposeScheduleView extends ConstraintLayout { resetScheduleButton = findViewById(R.id.resetScheduleButton); scheduledDateTimeView = findViewById(R.id.scheduledDateTime); + invalidScheduleWarningView = findViewById(R.id.invalidScheduleWarning); scheduledDateTimeView.setOnClickListener(v -> openPickDateDialog()); + invalidScheduleWarningView.setText(R.string.warning_scheduling_interval); scheduleDateTime = null; @@ -89,10 +95,13 @@ public class ComposeScheduleView extends ConstraintLayout { private void setScheduledDateTime() { if (scheduleDateTime == null) { scheduledDateTimeView.setText(""); + invalidScheduleWarningView.setVisibility(GONE); } else { + Date scheduled = scheduleDateTime.getTime(); scheduledDateTimeView.setText(String.format("%s %s", - dateFormat.format(scheduleDateTime.getTime()), - timeFormat.format(scheduleDateTime.getTime()))); + dateFormat.format(scheduled), + timeFormat.format(scheduled))); + verifyScheduledTime(scheduled); } } @@ -124,9 +133,7 @@ public class ComposeScheduleView extends ConstraintLayout { .setValidator( DateValidatorPointForward.from(yesterday)) .build(); - if (scheduleDateTime == null) { - scheduleDateTime = Calendar.getInstance(TimeZone.getDefault()); - } + initializeSuggestedTime(); MaterialDatePicker picker = MaterialDatePicker.Builder .datePicker() .setSelection(scheduleDateTime.getTimeInMillis()) @@ -147,6 +154,16 @@ public class ComposeScheduleView extends ConstraintLayout { picker.show(((AppCompatActivity) getContext()).getSupportFragmentManager(), "time_picker"); } + public Date getDateTime(String scheduledAt) { + if (scheduledAt != null) { + try { + return iso8601.parse(scheduledAt); + } catch (ParseException e) { + } + } + return null; + } + public void setDateTime(String scheduledAt) { Date date; try { @@ -154,27 +171,34 @@ public class ComposeScheduleView extends ConstraintLayout { } catch (ParseException e) { return; } - if (scheduleDateTime == null) { - scheduleDateTime = Calendar.getInstance(TimeZone.getDefault()); - } + initializeSuggestedTime(); scheduleDateTime.setTime(date); setScheduledDateTime(); } - private void onDateSet(long selection) { - if (scheduleDateTime == null) { - scheduleDateTime = Calendar.getInstance(TimeZone.getDefault()); + public boolean verifyScheduledTime(@Nullable Date scheduledTime) { + boolean valid; + if (scheduledTime != null) { + Calendar minimumScheduledTime = getCalendar(); + minimumScheduledTime.add(Calendar.SECOND, MINIMUM_SCHEDULED_SECONDS); + valid = scheduledTime.after(minimumScheduledTime.getTime()); + } else { + valid = true; } - Calendar newDate = Calendar.getInstance(TimeZone.getDefault()); + invalidScheduleWarningView.setVisibility(valid ? GONE : VISIBLE); + return valid; + } + + private void onDateSet(long selection) { + initializeSuggestedTime(); + Calendar newDate = getCalendar(); newDate.setTimeInMillis(selection); scheduleDateTime.set(newDate.get(Calendar.YEAR), newDate.get(Calendar.MONTH), newDate.get(Calendar.DATE)); openPickTimeDialog(); } public void onTimeSet(int hourOfDay, int minute) { - if (scheduleDateTime == null) { - scheduleDateTime = Calendar.getInstance(TimeZone.getDefault()); - } + initializeSuggestedTime(); scheduleDateTime.set(Calendar.HOUR_OF_DAY, hourOfDay); scheduleDateTime.set(Calendar.MINUTE, minute); setScheduledDateTime(); @@ -186,4 +210,16 @@ public class ComposeScheduleView extends ConstraintLayout { } return iso8601.format(scheduleDateTime.getTime()); } + + @NonNull + public static Calendar getCalendar() { + return Calendar.getInstance(TimeZone.getDefault()); + } + + private void initializeSuggestedTime() { + if (scheduleDateTime == null) { + scheduleDateTime = getCalendar(); + scheduleDateTime.add(Calendar.MINUTE, 15); + } + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt index 1aab8ffb7..dfe554c9f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt @@ -20,8 +20,8 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.fragment.app.viewModels import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders import androidx.paging.PagedList import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DividerItemDecoration @@ -50,15 +50,13 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res @Inject lateinit var db: AppDatabase - private lateinit var viewModel: ConversationsViewModel + private val viewModel: ConversationsViewModel by viewModels { viewModelFactory } private lateinit var adapter: ConversationAdapter private var layoutManager: LinearLayoutManager? = null override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - viewModel = ViewModelProviders.of(this, viewModelFactory)[ConversationsViewModel::class.java] - return inflater.inflate(R.layout.fragment_timeline, container, false) } @@ -87,10 +85,10 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res initSwipeToRefresh() - viewModel.conversations.observe(this, Observer> { + viewModel.conversations.observe(viewLifecycleOwner, Observer> { adapter.submitList(it) }) - viewModel.networkState.observe(this, Observer { + viewModel.networkState.observe(viewLifecycleOwner, Observer { adapter.setNetworkState(it) }) @@ -99,7 +97,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } private fun initSwipeToRefresh() { - viewModel.refreshState.observe(this, Observer { + viewModel.refreshState.observe(viewLifecycleOwner, Observer { swipeRefreshLayout.isRefreshing = it == NetworkState.LOADING }) swipeRefreshLayout.setOnRefreshListener { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt index 2cf2a0915..e9483e474 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt @@ -19,14 +19,12 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.view.MenuItem -import androidx.appcompat.content.res.AppCompatResources +import androidx.activity.viewModels import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.report.adapter.ReportPagerAdapter import com.keylesspalace.tusky.di.ViewModelFactory -import com.keylesspalace.tusky.util.ThemeUtils import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector import kotlinx.android.synthetic.main.activity_report.* @@ -42,11 +40,10 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector { @Inject lateinit var viewModelFactory: ViewModelFactory - private lateinit var viewModel: ReportViewModel + private val viewModel: ReportViewModel by viewModels { viewModelFactory } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - viewModel = ViewModelProviders.of(this, viewModelFactory)[ReportViewModel::class.java] val accountId = intent?.getStringExtra(ACCOUNT_ID) val accountUserName = intent?.getStringExtra(ACCOUNT_USERNAME) if (accountId.isNullOrBlank() || accountUserName.isNullOrBlank()) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt index 95bf73561..611231ecd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt @@ -21,8 +21,8 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.report.ReportViewModel import com.keylesspalace.tusky.components.report.Screen @@ -40,12 +40,7 @@ class ReportDoneFragment : Fragment(), Injectable { @Inject lateinit var viewModelFactory: ViewModelFactory - private lateinit var viewModel: ReportViewModel - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory)[ReportViewModel::class.java] - } + private val viewModel: ReportViewModel by viewModels({ requireActivity() }) { viewModelFactory } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -69,8 +64,8 @@ class ReportDoneFragment : Fragment(), Injectable { progressMute.hide() } - buttonMute.setText(when { - it.data == true -> R.string.action_unmute + buttonMute.setText(when (it.data) { + true -> R.string.action_unmute else -> R.string.action_mute }) }) @@ -84,8 +79,8 @@ class ReportDoneFragment : Fragment(), Injectable { buttonBlock.hide() progressBlock.hide() } - buttonBlock.setText(when { - it.data == true -> R.string.action_unblock + buttonBlock.setText(when (it.data) { + true -> R.string.action_unblock else -> R.string.action_block }) }) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt index 9b0be6544..5a94ba176 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt @@ -21,8 +21,8 @@ import android.view.View import android.view.ViewGroup import androidx.core.widget.doAfterTextChanged import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.report.ReportViewModel @@ -39,12 +39,7 @@ class ReportNoteFragment : Fragment(), Injectable { @Inject lateinit var viewModelFactory: ViewModelFactory - private lateinit var viewModel: ReportViewModel - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory)[ReportViewModel::class.java] - } + private val viewModel: ReportViewModel by viewModels({ requireActivity() }) { viewModelFactory } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt index 43b229e3d..77534cf53 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt @@ -22,8 +22,8 @@ import android.view.ViewGroup import androidx.core.app.ActivityOptionsCompat import androidx.core.view.ViewCompat import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders import androidx.paging.PagedList import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DividerItemDecoration @@ -59,19 +59,13 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler { @Inject lateinit var accountManager: AccountManager - private lateinit var viewModel: ReportViewModel + private val viewModel: ReportViewModel by viewModels({ requireActivity() }) { viewModelFactory } private lateinit var adapter: StatusesAdapter private lateinit var layoutManager: LinearLayoutManager private var snackbarErrorRetry: Snackbar? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory)[ReportViewModel::class.java] - } - override fun showMedia(v: View?, status: Status?, idx: Int) { status?.actionableStatus?.let { actionable -> when (actionable.attachments[idx].type) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt index 1d3f51e28..2d26dba89 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt @@ -21,8 +21,8 @@ import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem +import androidx.activity.viewModels import androidx.appcompat.widget.SearchView -import androidx.lifecycle.ViewModelProviders import com.google.android.material.tabs.TabLayoutMediator import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.R @@ -40,12 +40,11 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector { @Inject lateinit var viewModelFactory: ViewModelFactory - private lateinit var viewModel: SearchViewModel + private val viewModel: SearchViewModel by viewModels { viewModelFactory } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_search) - viewModel = ViewModelProviders.of(this, viewModelFactory)[SearchViewModel::class.java] setSupportActionBar(toolbar) supportActionBar?.apply { setDisplayHomeAsUpEnabled(true) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt index a545a3902..5088b1827 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt @@ -5,9 +5,9 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels import androidx.lifecycle.LiveData import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders import androidx.paging.PagedList import androidx.paging.PagedListAdapter import androidx.recyclerview.widget.DividerItemDecoration @@ -30,11 +30,12 @@ import javax.inject.Inject abstract class SearchFragment : Fragment(), LinkListener, Injectable, SwipeRefreshLayout.OnRefreshListener { - private var snackbarErrorRetry: Snackbar? = null @Inject lateinit var viewModelFactory: ViewModelFactory - protected lateinit var viewModel: SearchViewModel + protected val viewModel: SearchViewModel by viewModels({ requireActivity() }) { viewModelFactory } + + private var snackbarErrorRetry: Snackbar? = null abstract fun createAdapter(): PagedListAdapter @@ -43,11 +44,6 @@ abstract class SearchFragment : Fragment(), abstract val data: LiveData> protected lateinit var adapter: PagedListAdapter - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory)[SearchViewModel::class.java] - } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_search, container, false) } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt index b02d8c75a..5293bd03a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt @@ -18,6 +18,10 @@ package com.keylesspalace.tusky.db import android.util.Log import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Status +import java.util.* +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.Comparator /** * This class caches the account database and handles all account related operations @@ -26,7 +30,8 @@ import com.keylesspalace.tusky.entity.Status private const val TAG = "AccountManager" -class AccountManager(db: AppDatabase) { +@Singleton +class AccountManager @Inject constructor(db: AppDatabase) { @Volatile var activeAccount: AccountEntity? = null @@ -60,7 +65,7 @@ class AccountManager(db: AppDatabase) { val maxAccountId = accounts.maxBy { it.id }?.id ?: 0 val newAccountId = maxAccountId + 1 - activeAccount = AccountEntity(id = newAccountId, domain = domain.toLowerCase(), accessToken = accessToken, isActive = true) + activeAccount = AccountEntity(id = newAccountId, domain = domain.toLowerCase(Locale.ROOT), accessToken = accessToken, isActive = true) } @@ -146,8 +151,8 @@ class AccountManager(db: AppDatabase) { saveAccount(it) } - activeAccount = accounts.find { acc -> - acc.id == accountId + activeAccount = accounts.find { (id) -> + id == accountId } activeAccount?.let { @@ -185,8 +190,8 @@ class AccountManager(db: AppDatabase) { * @return the requested account or null if it was not found */ fun getAccountById(accountId: Long): AccountEntity? { - return accounts.find { acc -> - acc.id == accountId + return accounts.find { (id) -> + id == accountId } } diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt index bbc870614..69194c795 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt @@ -21,10 +21,10 @@ import android.content.Context import android.content.SharedPreferences import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.preference.PreferenceManager +import androidx.room.Room import com.keylesspalace.tusky.TuskyApplication import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.EventHubImpl -import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.TimelineCases @@ -64,20 +64,23 @@ class AppModule { return TimelineCasesImpl(api, eventHub) } - @Provides - @Singleton - fun providesAccountManager(app: TuskyApplication): AccountManager { - return app.serviceLocator.get(AccountManager::class.java) - } - @Provides @Singleton fun providesEventHub(): EventHub = EventHubImpl @Provides @Singleton - fun providesDatabase(app: TuskyApplication): AppDatabase { - return app.serviceLocator.get(AppDatabase::class.java) + fun providesDatabase(appContext: Context): AppDatabase { + return Room.databaseBuilder(appContext, AppDatabase::class.java, "tuskyDB") + .allowMainThreadQueries() + .addMigrations(AppDatabase.MIGRATION_2_3, AppDatabase.MIGRATION_3_4, AppDatabase.MIGRATION_4_5, + AppDatabase.MIGRATION_5_6, AppDatabase.MIGRATION_6_7, AppDatabase.MIGRATION_7_8, + AppDatabase.MIGRATION_8_9, AppDatabase.MIGRATION_9_10, AppDatabase.MIGRATION_10_11, + AppDatabase.MIGRATION_11_12, AppDatabase.MIGRATION_12_13, AppDatabase.MIGRATION_10_13, + AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16, + AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19, + AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21) + .build() } @Provides diff --git a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt index 0bffc770c..cdfa1b44a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt @@ -13,14 +13,12 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ - package com.keylesspalace.tusky.di import android.content.Context import android.text.Spanned import com.google.gson.Gson import com.google.gson.GsonBuilder -import com.google.gson.JsonDeserializer import com.keylesspalace.tusky.BuildConfig import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.json.SpannedTypeAdapter @@ -30,13 +28,9 @@ import com.keylesspalace.tusky.network.NotestockApi import com.keylesspalace.tusky.util.OkHttpUtils import dagger.Module import dagger.Provides -import dagger.multibindings.ClassKey -import dagger.multibindings.IntoMap -import dagger.multibindings.IntoSet import net.accelf.yuito.HttpToastInterceptor import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor -import retrofit2.Converter import retrofit2.Retrofit import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory import retrofit2.converter.gson.GsonConverterFactory @@ -49,32 +43,20 @@ import javax.inject.Singleton @Module class NetworkModule { - @Provides - @IntoMap - @ClassKey(Spanned::class) - fun providesSpannedTypeAdapter(): JsonDeserializer<*> = SpannedTypeAdapter() - @Provides @Singleton - fun providesGson(adapters: @JvmSuppressWildcards Map, JsonDeserializer<*>>): Gson { + fun providesGson(): Gson { return GsonBuilder() - .apply { - for ((k, v) in adapters) { - registerTypeAdapter(k, v) - } - } + .registerTypeAdapter(Spanned::class.java, SpannedTypeAdapter()) .create() } @Provides - @IntoSet @Singleton - fun providesConverterFactory(gson: Gson): Converter.Factory = GsonConverterFactory.create(gson) - - @Provides - @Singleton - fun providesHttpClient(accountManager: AccountManager, - context: Context): OkHttpClient { + fun providesHttpClient( + accountManager: AccountManager, + context: Context + ): OkHttpClient { return OkHttpUtils.getCompatibleClientBuilder(context) .apply { addInterceptor(InstanceSwitchAuthInterceptor(accountManager)) @@ -88,18 +70,14 @@ class NetworkModule { @Provides @Singleton - fun providesRetrofit(httpClient: OkHttpClient, - converters: @JvmSuppressWildcards Set): Retrofit { + fun providesRetrofit( + httpClient: OkHttpClient, + gson: Gson + ): Retrofit { return Retrofit.Builder().baseUrl("https://" + MastodonApi.PLACEHOLDER_DOMAIN) .client(httpClient) - .let { builder -> - // Doing it this way in case builder will be immutable so we return the final - // instance - converters.fold(builder) { b, c -> - b.addConverterFactory(c) - } - builder.addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync()) - } + .addConverterFactory(GsonConverterFactory.create(gson)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync()) .build() } @@ -111,7 +89,7 @@ class NetworkModule { @Provides @Singleton fun providesNotestockApi(context: Context, - converters: @JvmSuppressWildcards Set): NotestockApi { + gson: Gson): NotestockApi { val httpClient = OkHttpUtils.getCompatibleClientBuilder(context) .apply { if (BuildConfig.DEBUG) { @@ -122,12 +100,8 @@ class NetworkModule { .build() val retrofit = Retrofit.Builder().baseUrl("https://notestock.osa-p.net") .client(httpClient) - .let { builder -> - converters.fold(builder) { b, c -> - b.addConverterFactory(c) - } - builder.addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync()) - } + .addConverterFactory(GsonConverterFactory.create(gson)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync()) .build() return retrofit.create(NotestockApi::class.java) } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java index 84a0292af..5e5920a81 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -167,6 +167,7 @@ public class NotificationsFragment extends SFragment implements private boolean alwaysShowSensitiveMedia; private boolean alwaysOpenSpoiler; private boolean showNotificationsFilter; + private boolean showingError; // Each element is either a Notification for loading data or a Placeholder private final PairedList, NotificationViewData> notifications @@ -280,7 +281,7 @@ public class NotificationsFragment extends SFragment implements private void updateFilterVisibility() { CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams(); - if (showNotificationsFilter) { + if (showNotificationsFilter && !showingError && !notifications.isEmpty()) { appBarOptions.setExpanded(true, false); appBarOptions.setVisibility(View.VISIBLE); //Set content behaviour to hide filter on scroll @@ -392,6 +393,7 @@ public class NotificationsFragment extends SFragment implements @Override public void onRefresh() { this.statusView.setVisibility(View.GONE); + this.showingError = false; Either first = CollectionsKt.firstOrNull(this.notifications); String topId; if (first != null && first.isRight()) { @@ -674,6 +676,7 @@ public class NotificationsFragment extends SFragment implements //Show friend elephant this.statusView.setVisibility(View.VISIBLE); this.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null); + updateFilterVisibility(); //Update adapter updateAdapter(); @@ -999,6 +1002,7 @@ public class NotificationsFragment extends SFragment implements } else { swipeRefreshLayout.setEnabled(true); } + updateFilterVisibility(); swipeRefreshLayout.setRefreshing(false); progressBar.setVisibility(View.GONE); } @@ -1014,6 +1018,7 @@ public class NotificationsFragment extends SFragment implements } else if (this.notifications.isEmpty()) { this.statusView.setVisibility(View.VISIBLE); swipeRefreshLayout.setEnabled(false); + this.showingError = true; if (exception instanceof IOException) { this.statusView.setup(R.drawable.elephant_offline, R.string.error_network, __ -> { this.progressBar.setVisibility(View.VISIBLE); @@ -1027,6 +1032,7 @@ public class NotificationsFragment extends SFragment implements return Unit.INSTANCE; }); } + updateFilterVisibility(); } Log.e(TAG, "Fetch failure: " + exception.getMessage()); diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java index 0a5e2d71e..cb60fb2bf 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java @@ -275,6 +275,7 @@ public abstract class SFragment extends BaseFragment implements Injectable { " - " + statusToShare.getContent().toString(); sendIntent.putExtra(Intent.EXTRA_TEXT, stringToShare); + sendIntent.putExtra(Intent.EXTRA_SUBJECT, statusUrl); sendIntent.setType("text/plain"); startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_status_content_to))); return true; diff --git a/app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt b/app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt index e39566f4b..4789ac3c1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt +++ b/app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt @@ -39,7 +39,7 @@ class BackgroundMessageView @JvmOverloads constructor( fun setup(@DrawableRes imageRes: Int, @StringRes messageRes: Int, clickListener: ((v: View) -> Unit)? = null) { messageTextView.setText(messageRes) - messageTextView.setCompoundDrawablesWithIntrinsicBounds(0, imageRes, 0, 0) + imageView.setImageResource(imageRes) button.setOnClickListener(clickListener) button.visible(clickListener != null) } diff --git a/app/src/main/res/layout/activity_account_list.xml b/app/src/main/res/layout/activity_account_list.xml index 67cb0a7e3..fb6174977 100644 --- a/app/src/main/res/layout/activity_account_list.xml +++ b/app/src/main/res/layout/activity_account_list.xml @@ -9,7 +9,7 @@ - + app:layout_constraintTop_toBottomOf="@id/appbar" + tools:visibility="visible" + app:layout_constrainedHeight="true" /> diff --git a/app/src/main/res/layout/activity_modal_timeline.xml b/app/src/main/res/layout/activity_modal_timeline.xml index 1687467d8..2a956cf66 100644 --- a/app/src/main/res/layout/activity_modal_timeline.xml +++ b/app/src/main/res/layout/activity_modal_timeline.xml @@ -15,7 +15,7 @@ - - - - - + tools:visibility="visible" + app:layout_constrainedHeight="true" /> + xmlns:tools="http://schemas.android.com/tools" + tools:gravity="center_horizontal" + tools:orientation="vertical" + tools:parentTag="android.widget.LinearLayout"> + +