Merge remote-tracking branch 'tuskyapp/develop'

This commit is contained in:
kyori19 2020-02-26 21:09:20 +09:00
commit 5503c801c1
51 changed files with 468 additions and 415 deletions

View File

@ -102,12 +102,12 @@ project.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
} }
} }
ext.lifecycleVersion = "2.1.0" ext.lifecycleVersion = "2.2.0"
ext.roomVersion = '2.2.3' ext.roomVersion = '2.2.4'
ext.retrofitVersion = '2.6.0' ext.retrofitVersion = '2.7.1'
ext.okhttpVersion = '4.3.1' ext.okhttpVersion = '4.3.1'
ext.glideVersion = '4.10.0' ext.glideVersion = '4.10.0'
ext.daggerVersion = '2.25.3' ext.daggerVersion = '2.26'
repositories { repositories {
maven { maven {
@ -119,9 +119,9 @@ repositories {
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 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.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.browser:browser:1.2.0"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
implementation "androidx.recyclerview:recyclerview:1.1.0" implementation "androidx.recyclerview:recyclerview:1.1.0"
@ -131,8 +131,9 @@ dependencies {
implementation "androidx.sharetarget:sharetarget:1.0.0-rc01" implementation "androidx.sharetarget:sharetarget:1.0.0-rc01"
implementation "androidx.emoji:emoji:1.0.0" implementation "androidx.emoji:emoji:1.0.0"
implementation "androidx.emoji:emoji-appcompat:1.0.0" implementation "androidx.emoji:emoji-appcompat:1.0.0"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-reactivestreams:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycleVersion"
implementation "androidx.constraintlayout:constraintlayout:1.1.3" implementation "androidx.constraintlayout:constraintlayout:1.1.3"
implementation "androidx.paging:paging-runtime-ktx:2.1.1" implementation "androidx.paging:paging-runtime-ktx:2.1.1"
implementation "androidx.viewpager2:viewpager2:1.0.0" implementation "androidx.viewpager2:viewpager2:1.0.0"
@ -140,7 +141,7 @@ dependencies {
implementation "androidx.room:room-rxjava2:$roomVersion" implementation "androidx.room:room-rxjava2:$roomVersion"
kapt "androidx.room:room-compiler:$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:retrofit:$retrofitVersion"
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion" implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"

View File

@ -27,6 +27,7 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.viewModels
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.Px import androidx.annotation.Px
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
@ -34,7 +35,6 @@ import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.emoji.text.EmojiCompat import androidx.emoji.text.EmojiCompat
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.viewpager2.widget.MarginPageTransformer import androidx.viewpager2.widget.MarginPageTransformer
@ -76,7 +76,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
private lateinit var viewModel: AccountViewModel private val viewModel: AccountViewModel by viewModels { viewModelFactory }
private val accountFieldAdapter = AccountFieldAdapter(this) private val accountFieldAdapter = AccountFieldAdapter(this)
@ -116,9 +116,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
loadResources() loadResources()
makeNotificationBarTransparent() makeNotificationBarTransparent()
setContentView(R.layout.activity_account) setContentView(R.layout.activity_account)
viewModel = ViewModelProviders.of(this, viewModelFactory)[AccountViewModel::class.java]
// Obtain information to fill out the profile. // Obtain information to fill out the profile.
viewModel.setAccountInfo(intent.getStringExtra(KEY_ACCOUNT_ID)!!) viewModel.setAccountInfo(intent.getStringExtra(KEY_ACCOUNT_ID)!!)

View File

@ -92,7 +92,7 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
@Override @Override
protected void attachBaseContext(Context base) { protected void attachBaseContext(Context base) {
super.attachBaseContext(TuskyApplication.localeManager.setLocale(base)); super.attachBaseContext(TuskyApplication.getLocaleManager().setLocale(base));
} }
protected boolean requiresLogin() { protected boolean requiresLogin() {

View File

@ -178,11 +178,13 @@ abstract class BottomSheetActivity : BaseActivity() {
// https://mastodon.foo.bar/@User // https://mastodon.foo.bar/@User
// https://mastodon.foo.bar/@User/43456787654678 // https://mastodon.foo.bar/@User/43456787654678
// https://pleroma.foo.bar/users/User // https://pleroma.foo.bar/users/User
// https://pleroma.foo.bar/users/43456787654678 // https://pleroma.foo.bar/users/9qTHT2ANWUdXzENqC0
// https://pleroma.foo.bar/notice/43456787654678 // https://pleroma.foo.bar/notice/9sBHWIlwwGZi5QGlHc
// https://pleroma.foo.bar/objects/d4643c42-3ae0-4b73-b8b0-c725f5819207 // 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://mastodon.foo.bar/users/User/statuses/000000000000000000
// https://new.misskey.foo.bar/notes/012789abyz
// username@example.com // username@example.com
fun looksLikeMastodonUrl(urlString: String): Boolean { fun looksLikeMastodonUrl(urlString: String): Boolean {
val uri: URI val uri: URI
@ -200,14 +202,15 @@ fun looksLikeMastodonUrl(urlString: String): Boolean {
val path = uri.path val path = uri.path
return path.matches("^/@[^/]+$".toRegex()) || return path.matches("^/@[^/]+$".toRegex()) ||
path.matches("^/users/[^/]+$".toRegex()) ||
path.matches("^/@[^/]+/\\d+$".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("^/objects/[-a-f0-9]+$".toRegex()) ||
path.matches("^/users/[^/]+/statuses/[0-9]+$".toRegex()) ||
path.matches("^/notes/[a-z0-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()) path.matches("^[^@]+@[^@]+$".toRegex())
} }
enum class PostLookupFallbackBehavior { enum class PostLookupFallbackBehavior {

View File

@ -19,7 +19,6 @@ import android.Manifest
import android.app.Activity import android.app.Activity
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.Bitmap import android.graphics.Bitmap
@ -34,6 +33,7 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import androidx.activity.viewModels
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.FitCenter import com.bumptech.glide.load.resource.bitmap.FitCenter
import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.bumptech.glide.load.resource.bitmap.RoundedCorners
@ -69,7 +69,7 @@ class EditProfileActivity : BaseActivity(), Injectable {
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
private lateinit var viewModel: EditProfileViewModel private val viewModel: EditProfileViewModel by viewModels { viewModelFactory }
private var currentlyPicking: PickType = PickType.NOTHING private var currentlyPicking: PickType = PickType.NOTHING
@ -90,8 +90,6 @@ class EditProfileActivity : BaseActivity(), Injectable {
setContentView(R.layout.activity_edit_profile) setContentView(R.layout.activity_edit_profile)
viewModel = ViewModelProviders.of(this, viewModelFactory)[EditProfileViewModel::class.java]
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
supportActionBar?.run { supportActionBar?.run {
setTitle(R.string.title_edit_profile) setTitle(R.string.title_edit_profile)

View File

@ -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 <http://www.gnu.org/licenses>. */
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<Object> 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> T get(Class<T> 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<Object> androidInjector() {
return androidInjector;
}
public interface ServiceLocator {
<T> T get(Class<T> clazz);
}
}

View File

@ -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 <http://www.gnu.org/licenses>. */
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<Any>
@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
}
}

View File

@ -39,6 +39,7 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.* import android.widget.*
import androidx.activity.viewModels
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
@ -52,7 +53,6 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doAfterTextChanged
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager 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.makeCaptionDialog
import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog
import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener 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.db.AccountEntity
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory 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 // this only exists when a status is trying to be sent, but uploads are still occurring
private var finishingUploadDialog: ProgressDialog? = null private var finishingUploadDialog: ProgressDialog? = null
private var currentInputContentInfo: InputContentInfoCompat? = null
private var currentFlags: Int = 0
private var photoUploadUri: Uri? = null private var photoUploadUri: Uri? = null
@VisibleForTesting @VisibleForTesting
var maximumTootCharacters = DEFAULT_CHARACTER_LIMIT var maximumTootCharacters = DEFAULT_CHARACTER_LIMIT
private var composeOptions: ComposeOptions? = null private var composeOptions: ComposeOptions? = null
private lateinit var viewModel: ComposeViewModel private val viewModel: ComposeViewModel by viewModels { viewModelFactory }
private var mediaCount = 0 private var mediaCount = 0
@ -149,11 +148,11 @@ class ComposeActivity : BaseActivity(),
composeMediaPreviewBar.adapter = mediaAdapter composeMediaPreviewBar.adapter = mediaAdapter
composeMediaPreviewBar.itemAnimator = null composeMediaPreviewBar.itemAnimator = null
viewModel = ViewModelProviders.of(this, viewModelFactory)[ComposeViewModel::class.java]
subscribeToUpdates(mediaAdapter) subscribeToUpdates(mediaAdapter)
setupButtons() setupButtons()
photoUploadUri = savedInstanceState?.getParcelable(PHOTO_UPLOAD_URI_KEY)
/* If the composer is started up as a reply to another post, override the "starting" state /* 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. */ * based on what the intent from the reply request passes. */
if (intent != null) { if (intent != null) {
@ -544,14 +543,7 @@ class ComposeActivity : BaseActivity(),
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
if (currentInputContentInfo != null) { outState.putParcelable(PHOTO_UPLOAD_URI_KEY, photoUploadUri)
outState.putParcelable("commitContentInputContentInfo",
currentInputContentInfo!!.unwrap() as Parcelable?)
outState.putInt("commitContentFlags", currentFlags)
}
currentInputContentInfo = null
currentFlags = 0
outState.putParcelable("photoUploadUri", photoUploadUri)
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
} }
@ -777,47 +769,42 @@ class ComposeActivity : BaseActivity(),
updateVisibleCharactersLeft() updateVisibleCharactersLeft()
} }
private fun verifyScheduledTime(): Boolean {
return composeScheduleView.verifyScheduledTime(composeScheduleView.getDateTime(viewModel.scheduledAt.value))
}
private fun onSendClicked() { private fun onSendClicked() {
enableButtons(false) if (verifyScheduledTime()) {
sendStatus() sendStatus()
} else {
showScheduleView()
}
} }
/** This is for the fancy keyboards which can insert images and stuff. */ /** This is for the fancy keyboards which can insert images and stuff. */
override fun onCommitContent(inputContentInfo: InputContentInfoCompat, flags: Int, opts: Bundle): Boolean { 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
}
// Verify the returned content's type is of the correct MIME type // Verify the returned content's type is of the correct MIME type
val supported = inputContentInfo.description.hasMimeType("image/*") val supported = inputContentInfo.description.hasMimeType("image/*")
return supported && onCommitContentInternal(inputContentInfo, flags) if(supported) {
} val lacksPermission = (flags and InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0
if(lacksPermission) {
private fun onCommitContentInternal(inputContentInfo: InputContentInfoCompat, flags: Int): Boolean { try {
if (flags and InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION != 0) { inputContentInfo.requestPermission()
try { } catch (e: Exception) {
inputContentInfo.requestPermission() Log.e(TAG, "InputContentInfoCompat#requestPermission() failed." + e.message)
} catch (e: Exception) { return false
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. return false
pickMedia(inputContentInfo.contentUri)
currentInputContentInfo = inputContentInfo
currentFlags = flags
return true
} }
private fun sendStatus() { private fun sendStatus() {
enableButtons(false)
var contentText = composeEditField.text.toString() var contentText = composeEditField.text.toString()
var spoilerText = "" var spoilerText = ""
if (viewModel.showContentWarning.value!!) { 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 { withLifecycleContext {
viewModel.pickMedia(uri).observe { exceptionOrItem -> viewModel.pickMedia(uri).observe { exceptionOrItem ->
contentInfoCompat?.releasePermission()
exceptionOrItem.asLeftOrNull()?.let { exceptionOrItem.asLeftOrNull()?.let {
val errorId = when (it) { val errorId = when (it) {
is VideoSizeException -> { is VideoSizeException -> {
@ -1069,7 +1059,11 @@ class ComposeActivity : BaseActivity(),
override fun onTimeSet(view: TimePicker, hourOfDay: Int, minute: Int) { override fun onTimeSet(view: TimePicker, hourOfDay: Int, minute: Int) {
composeScheduleView.onTimeSet(hourOfDay, minute) composeScheduleView.onTimeSet(hourOfDay, minute)
viewModel.updateScheduledAt(composeScheduleView.time) viewModel.updateScheduledAt(composeScheduleView.time)
scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN if (verifyScheduledTime()) {
scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN
} else {
showScheduleView()
}
} }
private fun resetSchedule() { private fun resetSchedule() {
@ -1107,6 +1101,7 @@ class ComposeActivity : BaseActivity(),
private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1 private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1
private const val COMPOSE_OPTIONS_EXTRA = "COMPOSE_OPTIONS" 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 // Mastodon only counts URLs as this long in terms of status character limits
@VisibleForTesting @VisibleForTesting

View File

@ -22,6 +22,8 @@ import android.util.AttributeSet;
import android.widget.Button; import android.widget.Button;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintLayout;
@ -48,8 +50,10 @@ public class ComposeScheduleView extends ConstraintLayout {
private Button resetScheduleButton; private Button resetScheduleButton;
private TextView scheduledDateTimeView; private TextView scheduledDateTimeView;
private TextView invalidScheduleWarningView;
private Calendar scheduleDateTime; private Calendar scheduleDateTime;
public static int MINIMUM_SCHEDULED_SECONDS = 330; // Minimum is 5 minutes, pad 30 seconds for posting
public ComposeScheduleView(Context context) { public ComposeScheduleView(Context context) {
super(context); super(context);
@ -76,8 +80,10 @@ public class ComposeScheduleView extends ConstraintLayout {
resetScheduleButton = findViewById(R.id.resetScheduleButton); resetScheduleButton = findViewById(R.id.resetScheduleButton);
scheduledDateTimeView = findViewById(R.id.scheduledDateTime); scheduledDateTimeView = findViewById(R.id.scheduledDateTime);
invalidScheduleWarningView = findViewById(R.id.invalidScheduleWarning);
scheduledDateTimeView.setOnClickListener(v -> openPickDateDialog()); scheduledDateTimeView.setOnClickListener(v -> openPickDateDialog());
invalidScheduleWarningView.setText(R.string.warning_scheduling_interval);
scheduleDateTime = null; scheduleDateTime = null;
@ -89,10 +95,13 @@ public class ComposeScheduleView extends ConstraintLayout {
private void setScheduledDateTime() { private void setScheduledDateTime() {
if (scheduleDateTime == null) { if (scheduleDateTime == null) {
scheduledDateTimeView.setText(""); scheduledDateTimeView.setText("");
invalidScheduleWarningView.setVisibility(GONE);
} else { } else {
Date scheduled = scheduleDateTime.getTime();
scheduledDateTimeView.setText(String.format("%s %s", scheduledDateTimeView.setText(String.format("%s %s",
dateFormat.format(scheduleDateTime.getTime()), dateFormat.format(scheduled),
timeFormat.format(scheduleDateTime.getTime()))); timeFormat.format(scheduled)));
verifyScheduledTime(scheduled);
} }
} }
@ -124,9 +133,7 @@ public class ComposeScheduleView extends ConstraintLayout {
.setValidator( .setValidator(
DateValidatorPointForward.from(yesterday)) DateValidatorPointForward.from(yesterday))
.build(); .build();
if (scheduleDateTime == null) { initializeSuggestedTime();
scheduleDateTime = Calendar.getInstance(TimeZone.getDefault());
}
MaterialDatePicker<Long> picker = MaterialDatePicker.Builder MaterialDatePicker<Long> picker = MaterialDatePicker.Builder
.datePicker() .datePicker()
.setSelection(scheduleDateTime.getTimeInMillis()) .setSelection(scheduleDateTime.getTimeInMillis())
@ -147,6 +154,16 @@ public class ComposeScheduleView extends ConstraintLayout {
picker.show(((AppCompatActivity) getContext()).getSupportFragmentManager(), "time_picker"); 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) { public void setDateTime(String scheduledAt) {
Date date; Date date;
try { try {
@ -154,27 +171,34 @@ public class ComposeScheduleView extends ConstraintLayout {
} catch (ParseException e) { } catch (ParseException e) {
return; return;
} }
if (scheduleDateTime == null) { initializeSuggestedTime();
scheduleDateTime = Calendar.getInstance(TimeZone.getDefault());
}
scheduleDateTime.setTime(date); scheduleDateTime.setTime(date);
setScheduledDateTime(); setScheduledDateTime();
} }
private void onDateSet(long selection) { public boolean verifyScheduledTime(@Nullable Date scheduledTime) {
if (scheduleDateTime == null) { boolean valid;
scheduleDateTime = Calendar.getInstance(TimeZone.getDefault()); 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); newDate.setTimeInMillis(selection);
scheduleDateTime.set(newDate.get(Calendar.YEAR), newDate.get(Calendar.MONTH), newDate.get(Calendar.DATE)); scheduleDateTime.set(newDate.get(Calendar.YEAR), newDate.get(Calendar.MONTH), newDate.get(Calendar.DATE));
openPickTimeDialog(); openPickTimeDialog();
} }
public void onTimeSet(int hourOfDay, int minute) { public void onTimeSet(int hourOfDay, int minute) {
if (scheduleDateTime == null) { initializeSuggestedTime();
scheduleDateTime = Calendar.getInstance(TimeZone.getDefault());
}
scheduleDateTime.set(Calendar.HOUR_OF_DAY, hourOfDay); scheduleDateTime.set(Calendar.HOUR_OF_DAY, hourOfDay);
scheduleDateTime.set(Calendar.MINUTE, minute); scheduleDateTime.set(Calendar.MINUTE, minute);
setScheduledDateTime(); setScheduledDateTime();
@ -186,4 +210,16 @@ public class ComposeScheduleView extends ConstraintLayout {
} }
return iso8601.format(scheduleDateTime.getTime()); 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);
}
}
} }

View File

@ -20,8 +20,8 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.paging.PagedList import androidx.paging.PagedList
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
@ -50,15 +50,13 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
@Inject @Inject
lateinit var db: AppDatabase lateinit var db: AppDatabase
private lateinit var viewModel: ConversationsViewModel private val viewModel: ConversationsViewModel by viewModels { viewModelFactory }
private lateinit var adapter: ConversationAdapter private lateinit var adapter: ConversationAdapter
private var layoutManager: LinearLayoutManager? = null private var layoutManager: LinearLayoutManager? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 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) return inflater.inflate(R.layout.fragment_timeline, container, false)
} }
@ -87,10 +85,10 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
initSwipeToRefresh() initSwipeToRefresh()
viewModel.conversations.observe(this, Observer<PagedList<ConversationEntity>> { viewModel.conversations.observe(viewLifecycleOwner, Observer<PagedList<ConversationEntity>> {
adapter.submitList(it) adapter.submitList(it)
}) })
viewModel.networkState.observe(this, Observer { viewModel.networkState.observe(viewLifecycleOwner, Observer {
adapter.setNetworkState(it) adapter.setNetworkState(it)
}) })
@ -99,7 +97,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
} }
private fun initSwipeToRefresh() { private fun initSwipeToRefresh() {
viewModel.refreshState.observe(this, Observer { viewModel.refreshState.observe(viewLifecycleOwner, Observer {
swipeRefreshLayout.isRefreshing = it == NetworkState.LOADING swipeRefreshLayout.isRefreshing = it == NetworkState.LOADING
}) })
swipeRefreshLayout.setOnRefreshListener { swipeRefreshLayout.setOnRefreshListener {

View File

@ -19,14 +19,12 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.content.res.AppCompatResources import androidx.activity.viewModels
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.BottomSheetActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.report.adapter.ReportPagerAdapter import com.keylesspalace.tusky.components.report.adapter.ReportPagerAdapter
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.util.ThemeUtils
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import kotlinx.android.synthetic.main.activity_report.* import kotlinx.android.synthetic.main.activity_report.*
@ -42,11 +40,10 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector {
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
private lateinit var viewModel: ReportViewModel private val viewModel: ReportViewModel by viewModels { viewModelFactory }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
viewModel = ViewModelProviders.of(this, viewModelFactory)[ReportViewModel::class.java]
val accountId = intent?.getStringExtra(ACCOUNT_ID) val accountId = intent?.getStringExtra(ACCOUNT_ID)
val accountUserName = intent?.getStringExtra(ACCOUNT_USERNAME) val accountUserName = intent?.getStringExtra(ACCOUNT_USERNAME)
if (accountId.isNullOrBlank() || accountUserName.isNullOrBlank()) { if (accountId.isNullOrBlank() || accountUserName.isNullOrBlank()) {

View File

@ -21,8 +21,8 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.report.ReportViewModel import com.keylesspalace.tusky.components.report.ReportViewModel
import com.keylesspalace.tusky.components.report.Screen import com.keylesspalace.tusky.components.report.Screen
@ -40,12 +40,7 @@ class ReportDoneFragment : Fragment(), Injectable {
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
private lateinit var viewModel: ReportViewModel private val viewModel: ReportViewModel by viewModels({ requireActivity() }) { viewModelFactory }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory)[ReportViewModel::class.java]
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? { savedInstanceState: Bundle?): View? {
@ -69,8 +64,8 @@ class ReportDoneFragment : Fragment(), Injectable {
progressMute.hide() progressMute.hide()
} }
buttonMute.setText(when { buttonMute.setText(when (it.data) {
it.data == true -> R.string.action_unmute true -> R.string.action_unmute
else -> R.string.action_mute else -> R.string.action_mute
}) })
}) })
@ -84,8 +79,8 @@ class ReportDoneFragment : Fragment(), Injectable {
buttonBlock.hide() buttonBlock.hide()
progressBlock.hide() progressBlock.hide()
} }
buttonBlock.setText(when { buttonBlock.setText(when (it.data) {
it.data == true -> R.string.action_unblock true -> R.string.action_unblock
else -> R.string.action_block else -> R.string.action_block
}) })
}) })

View File

@ -21,8 +21,8 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.report.ReportViewModel import com.keylesspalace.tusky.components.report.ReportViewModel
@ -39,12 +39,7 @@ class ReportNoteFragment : Fragment(), Injectable {
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
private lateinit var viewModel: ReportViewModel private val viewModel: ReportViewModel by viewModels({ requireActivity() }) { viewModelFactory }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory)[ReportViewModel::class.java]
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? { savedInstanceState: Bundle?): View? {

View File

@ -22,8 +22,8 @@ import android.view.ViewGroup
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.paging.PagedList import androidx.paging.PagedList
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
@ -59,19 +59,13 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler {
@Inject @Inject
lateinit var accountManager: AccountManager 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 adapter: StatusesAdapter
private lateinit var layoutManager: LinearLayoutManager private lateinit var layoutManager: LinearLayoutManager
private var snackbarErrorRetry: Snackbar? = null 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) { override fun showMedia(v: View?, status: Status?, idx: Int) {
status?.actionableStatus?.let { actionable -> status?.actionableStatus?.let { actionable ->
when (actionable.attachments[idx].type) { when (actionable.attachments[idx].type) {

View File

@ -21,8 +21,8 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.activity.viewModels
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.lifecycle.ViewModelProviders
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.BottomSheetActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
@ -40,12 +40,11 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector {
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
private lateinit var viewModel: SearchViewModel private val viewModel: SearchViewModel by viewModels { viewModelFactory }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search) setContentView(R.layout.activity_search)
viewModel = ViewModelProviders.of(this, viewModelFactory)[SearchViewModel::class.java]
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
supportActionBar?.apply { supportActionBar?.apply {
setDisplayHomeAsUpEnabled(true) setDisplayHomeAsUpEnabled(true)

View File

@ -5,9 +5,9 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.paging.PagedList import androidx.paging.PagedList
import androidx.paging.PagedListAdapter import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
@ -30,11 +30,12 @@ import javax.inject.Inject
abstract class SearchFragment<T> : Fragment(), abstract class SearchFragment<T> : Fragment(),
LinkListener, Injectable, SwipeRefreshLayout.OnRefreshListener { LinkListener, Injectable, SwipeRefreshLayout.OnRefreshListener {
private var snackbarErrorRetry: Snackbar? = null
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory 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<T, *> abstract fun createAdapter(): PagedListAdapter<T, *>
@ -43,11 +44,6 @@ abstract class SearchFragment<T> : Fragment(),
abstract val data: LiveData<PagedList<T>> abstract val data: LiveData<PagedList<T>>
protected lateinit var adapter: PagedListAdapter<T, *> protected lateinit var adapter: PagedListAdapter<T, *>
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? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_search, container, false) return inflater.inflate(R.layout.fragment_search, container, false)
} }

View File

@ -18,6 +18,10 @@ package com.keylesspalace.tusky.db
import android.util.Log import android.util.Log
import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Status 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 * 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" private const val TAG = "AccountManager"
class AccountManager(db: AppDatabase) { @Singleton
class AccountManager @Inject constructor(db: AppDatabase) {
@Volatile @Volatile
var activeAccount: AccountEntity? = null var activeAccount: AccountEntity? = null
@ -60,7 +65,7 @@ class AccountManager(db: AppDatabase) {
val maxAccountId = accounts.maxBy { it.id }?.id ?: 0 val maxAccountId = accounts.maxBy { it.id }?.id ?: 0
val newAccountId = maxAccountId + 1 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) saveAccount(it)
} }
activeAccount = accounts.find { acc -> activeAccount = accounts.find { (id) ->
acc.id == accountId id == accountId
} }
activeAccount?.let { activeAccount?.let {
@ -185,8 +190,8 @@ class AccountManager(db: AppDatabase) {
* @return the requested account or null if it was not found * @return the requested account or null if it was not found
*/ */
fun getAccountById(accountId: Long): AccountEntity? { fun getAccountById(accountId: Long): AccountEntity? {
return accounts.find { acc -> return accounts.find { (id) ->
acc.id == accountId id == accountId
} }
} }

View File

@ -21,10 +21,10 @@ import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.room.Room
import com.keylesspalace.tusky.TuskyApplication import com.keylesspalace.tusky.TuskyApplication
import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.EventHubImpl import com.keylesspalace.tusky.appstore.EventHubImpl
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.network.TimelineCases import com.keylesspalace.tusky.network.TimelineCases
@ -64,20 +64,23 @@ class AppModule {
return TimelineCasesImpl(api, eventHub) return TimelineCasesImpl(api, eventHub)
} }
@Provides
@Singleton
fun providesAccountManager(app: TuskyApplication): AccountManager {
return app.serviceLocator.get(AccountManager::class.java)
}
@Provides @Provides
@Singleton @Singleton
fun providesEventHub(): EventHub = EventHubImpl fun providesEventHub(): EventHub = EventHubImpl
@Provides @Provides
@Singleton @Singleton
fun providesDatabase(app: TuskyApplication): AppDatabase { fun providesDatabase(appContext: Context): AppDatabase {
return app.serviceLocator.get(AppDatabase::class.java) 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 @Provides

View File

@ -13,14 +13,12 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not, * You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */ * see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.di package com.keylesspalace.tusky.di
import android.content.Context import android.content.Context
import android.text.Spanned import android.text.Spanned
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.google.gson.JsonDeserializer
import com.keylesspalace.tusky.BuildConfig import com.keylesspalace.tusky.BuildConfig
import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.json.SpannedTypeAdapter import com.keylesspalace.tusky.json.SpannedTypeAdapter
@ -30,13 +28,9 @@ import com.keylesspalace.tusky.network.NotestockApi
import com.keylesspalace.tusky.util.OkHttpUtils import com.keylesspalace.tusky.util.OkHttpUtils
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import dagger.multibindings.IntoSet
import net.accelf.yuito.HttpToastInterceptor import net.accelf.yuito.HttpToastInterceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Converter
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
@ -49,32 +43,20 @@ import javax.inject.Singleton
@Module @Module
class NetworkModule { class NetworkModule {
@Provides
@IntoMap
@ClassKey(Spanned::class)
fun providesSpannedTypeAdapter(): JsonDeserializer<*> = SpannedTypeAdapter()
@Provides @Provides
@Singleton @Singleton
fun providesGson(adapters: @JvmSuppressWildcards Map<Class<*>, JsonDeserializer<*>>): Gson { fun providesGson(): Gson {
return GsonBuilder() return GsonBuilder()
.apply { .registerTypeAdapter(Spanned::class.java, SpannedTypeAdapter())
for ((k, v) in adapters) {
registerTypeAdapter(k, v)
}
}
.create() .create()
} }
@Provides @Provides
@IntoSet
@Singleton @Singleton
fun providesConverterFactory(gson: Gson): Converter.Factory = GsonConverterFactory.create(gson) fun providesHttpClient(
accountManager: AccountManager,
@Provides context: Context
@Singleton ): OkHttpClient {
fun providesHttpClient(accountManager: AccountManager,
context: Context): OkHttpClient {
return OkHttpUtils.getCompatibleClientBuilder(context) return OkHttpUtils.getCompatibleClientBuilder(context)
.apply { .apply {
addInterceptor(InstanceSwitchAuthInterceptor(accountManager)) addInterceptor(InstanceSwitchAuthInterceptor(accountManager))
@ -88,18 +70,14 @@ class NetworkModule {
@Provides @Provides
@Singleton @Singleton
fun providesRetrofit(httpClient: OkHttpClient, fun providesRetrofit(
converters: @JvmSuppressWildcards Set<Converter.Factory>): Retrofit { httpClient: OkHttpClient,
gson: Gson
): Retrofit {
return Retrofit.Builder().baseUrl("https://" + MastodonApi.PLACEHOLDER_DOMAIN) return Retrofit.Builder().baseUrl("https://" + MastodonApi.PLACEHOLDER_DOMAIN)
.client(httpClient) .client(httpClient)
.let { builder -> .addConverterFactory(GsonConverterFactory.create(gson))
// Doing it this way in case builder will be immutable so we return the final .addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync())
// instance
converters.fold(builder) { b, c ->
b.addConverterFactory(c)
}
builder.addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync())
}
.build() .build()
} }
@ -111,7 +89,7 @@ class NetworkModule {
@Provides @Provides
@Singleton @Singleton
fun providesNotestockApi(context: Context, fun providesNotestockApi(context: Context,
converters: @JvmSuppressWildcards Set<Converter.Factory>): NotestockApi { gson: Gson): NotestockApi {
val httpClient = OkHttpUtils.getCompatibleClientBuilder(context) val httpClient = OkHttpUtils.getCompatibleClientBuilder(context)
.apply { .apply {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
@ -122,12 +100,8 @@ class NetworkModule {
.build() .build()
val retrofit = Retrofit.Builder().baseUrl("https://notestock.osa-p.net") val retrofit = Retrofit.Builder().baseUrl("https://notestock.osa-p.net")
.client(httpClient) .client(httpClient)
.let { builder -> .addConverterFactory(GsonConverterFactory.create(gson))
converters.fold(builder) { b, c -> .addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync())
b.addConverterFactory(c)
}
builder.addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync())
}
.build() .build()
return retrofit.create(NotestockApi::class.java) return retrofit.create(NotestockApi::class.java)
} }

View File

@ -167,6 +167,7 @@ public class NotificationsFragment extends SFragment implements
private boolean alwaysShowSensitiveMedia; private boolean alwaysShowSensitiveMedia;
private boolean alwaysOpenSpoiler; private boolean alwaysOpenSpoiler;
private boolean showNotificationsFilter; private boolean showNotificationsFilter;
private boolean showingError;
// Each element is either a Notification for loading data or a Placeholder // Each element is either a Notification for loading data or a Placeholder
private final PairedList<Either<Placeholder, Notification>, NotificationViewData> notifications private final PairedList<Either<Placeholder, Notification>, NotificationViewData> notifications
@ -280,7 +281,7 @@ public class NotificationsFragment extends SFragment implements
private void updateFilterVisibility() { private void updateFilterVisibility() {
CoordinatorLayout.LayoutParams params = CoordinatorLayout.LayoutParams params =
(CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams(); (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams();
if (showNotificationsFilter) { if (showNotificationsFilter && !showingError && !notifications.isEmpty()) {
appBarOptions.setExpanded(true, false); appBarOptions.setExpanded(true, false);
appBarOptions.setVisibility(View.VISIBLE); appBarOptions.setVisibility(View.VISIBLE);
//Set content behaviour to hide filter on scroll //Set content behaviour to hide filter on scroll
@ -392,6 +393,7 @@ public class NotificationsFragment extends SFragment implements
@Override @Override
public void onRefresh() { public void onRefresh() {
this.statusView.setVisibility(View.GONE); this.statusView.setVisibility(View.GONE);
this.showingError = false;
Either<Placeholder, Notification> first = CollectionsKt.firstOrNull(this.notifications); Either<Placeholder, Notification> first = CollectionsKt.firstOrNull(this.notifications);
String topId; String topId;
if (first != null && first.isRight()) { if (first != null && first.isRight()) {
@ -674,6 +676,7 @@ public class NotificationsFragment extends SFragment implements
//Show friend elephant //Show friend elephant
this.statusView.setVisibility(View.VISIBLE); this.statusView.setVisibility(View.VISIBLE);
this.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null); this.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null);
updateFilterVisibility();
//Update adapter //Update adapter
updateAdapter(); updateAdapter();
@ -999,6 +1002,7 @@ public class NotificationsFragment extends SFragment implements
} else { } else {
swipeRefreshLayout.setEnabled(true); swipeRefreshLayout.setEnabled(true);
} }
updateFilterVisibility();
swipeRefreshLayout.setRefreshing(false); swipeRefreshLayout.setRefreshing(false);
progressBar.setVisibility(View.GONE); progressBar.setVisibility(View.GONE);
} }
@ -1014,6 +1018,7 @@ public class NotificationsFragment extends SFragment implements
} else if (this.notifications.isEmpty()) { } else if (this.notifications.isEmpty()) {
this.statusView.setVisibility(View.VISIBLE); this.statusView.setVisibility(View.VISIBLE);
swipeRefreshLayout.setEnabled(false); swipeRefreshLayout.setEnabled(false);
this.showingError = true;
if (exception instanceof IOException) { if (exception instanceof IOException) {
this.statusView.setup(R.drawable.elephant_offline, R.string.error_network, __ -> { this.statusView.setup(R.drawable.elephant_offline, R.string.error_network, __ -> {
this.progressBar.setVisibility(View.VISIBLE); this.progressBar.setVisibility(View.VISIBLE);
@ -1027,6 +1032,7 @@ public class NotificationsFragment extends SFragment implements
return Unit.INSTANCE; return Unit.INSTANCE;
}); });
} }
updateFilterVisibility();
} }
Log.e(TAG, "Fetch failure: " + exception.getMessage()); Log.e(TAG, "Fetch failure: " + exception.getMessage());

View File

@ -275,6 +275,7 @@ public abstract class SFragment extends BaseFragment implements Injectable {
" - " + " - " +
statusToShare.getContent().toString(); statusToShare.getContent().toString();
sendIntent.putExtra(Intent.EXTRA_TEXT, stringToShare); sendIntent.putExtra(Intent.EXTRA_TEXT, stringToShare);
sendIntent.putExtra(Intent.EXTRA_SUBJECT, statusUrl);
sendIntent.setType("text/plain"); sendIntent.setType("text/plain");
startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_status_content_to))); startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_status_content_to)));
return true; return true;

View File

@ -39,7 +39,7 @@ class BackgroundMessageView @JvmOverloads constructor(
fun setup(@DrawableRes imageRes: Int, @StringRes messageRes: Int, fun setup(@DrawableRes imageRes: Int, @StringRes messageRes: Int,
clickListener: ((v: View) -> Unit)? = null) { clickListener: ((v: View) -> Unit)? = null) {
messageTextView.setText(messageRes) messageTextView.setText(messageRes)
messageTextView.setCompoundDrawablesWithIntrinsicBounds(0, imageRes, 0, 0) imageView.setImageResource(imageRes)
button.setOnClickListener(clickListener) button.setOnClickListener(clickListener)
button.visible(clickListener != null) button.visible(clickListener != null)
} }

View File

@ -9,7 +9,7 @@
<include layout="@layout/toolbar_basic" /> <include layout="@layout/toolbar_basic" />
<FrameLayout <androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container" android:id="@+id/fragment_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View File

@ -39,7 +39,9 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toBottomOf="@id/appbar"
tools:visibility="visible"
app:layout_constrainedHeight="true" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -15,7 +15,7 @@
<include layout="@layout/toolbar_basic" /> <include layout="@layout/toolbar_basic" />
<FrameLayout <androidx.fragment.app.FragmentContainerView
android:id="@+id/contentFrame" android:id="@+id/contentFrame"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View File

@ -9,7 +9,7 @@
<include layout="@layout/toolbar_basic" /> <include layout="@layout/toolbar_basic" />
<FrameLayout <androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container" android:id="@+id/fragment_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View File

@ -15,7 +15,7 @@
<include layout="@layout/toolbar_basic" /> <include layout="@layout/toolbar_basic" />
<FrameLayout <androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container" android:id="@+id/fragment_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View File

@ -15,7 +15,7 @@
<include layout="@layout/toolbar_basic" /> <include layout="@layout/toolbar_basic" />
<FrameLayout <androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container" android:id="@+id/fragment_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View File

@ -9,7 +9,7 @@
<include layout="@layout/toolbar_basic" /> <include layout="@layout/toolbar_basic" />
<FrameLayout <androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container" android:id="@+id/fragment_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View File

@ -36,7 +36,8 @@
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" /> tools:visibility="visible"
app:layout_constrainedHeight="true" />
<androidx.core.widget.ContentLoadingProgressBar <androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/topProgressBar" android:id="@+id/topProgressBar"

View File

@ -53,7 +53,7 @@
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout" android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
app:layout_behavior="@string/appbar_scrolling_view_behavior"> app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView

View File

@ -1,6 +1,19 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android" <merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools"
tools:gravity="center_horizontal"
tools:orientation="vertical"
tools:parentTag="android.widget.LinearLayout">
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="4dp"
android:layout_weight="1"
android:contentDescription="@null"
android:scaleType="centerInside"
tools:src="@drawable/elephant_offline" />
<TextView <TextView
android:id="@+id/messageTextView" android:id="@+id/messageTextView"
@ -13,7 +26,6 @@
android:paddingRight="16dp" android:paddingRight="16dp"
android:textAlignment="center" android:textAlignment="center"
android:textSize="?attr/status_text_medium" android:textSize="?attr/status_text_medium"
tools:drawableTop="@drawable/elephant_offline"
tools:text="@string/error_network" /> tools:text="@string/error_network" />
<Button <Button
@ -22,5 +34,6 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginBottom="4dp"
android:text="@string/action_retry" /> android:text="@string/action_retry" />
</merge> </merge>

View File

@ -10,7 +10,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:text="@string/action_reset_schedule" android:text="@string/action_reset_schedule"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toTopOf="@id/invalidScheduleWarning"
app:layout_constraintStart_toStartOf="parent" /> app:layout_constraintStart_toStartOf="parent" />
<TextView <TextView
@ -23,10 +23,27 @@
android:paddingBottom="16dp" android:paddingBottom="16dp"
android:textColor="?android:textColorTertiary" android:textColor="?android:textColorTertiary"
android:textSize="?attr/status_text_medium" android:textSize="?attr/status_text_medium"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toTopOf="@id/invalidScheduleWarning"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1" app:layout_constraintHorizontal_bias="1"
app:layout_constraintStart_toEndOf="@id/resetScheduleButton" app:layout_constraintStart_toEndOf="@id/resetScheduleButton"
tools:text="2020/01/01 00:00:00" /> tools:text="2020/01/01 00:00:00" />
<TextView
android:id="@+id/invalidScheduleWarning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="4dp"
android:paddingStart="4dp"
android:paddingTop="4dp"
android:paddingBottom="16dp"
android:textColor="?android:textColorTertiary"
android:textSize="?attr/status_text_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintStart_toStartOf="parent"
tools:text="@string/warning_scheduling_interval"
android:visibility="gone" />
</merge> </merge>

View File

@ -469,5 +469,6 @@
<string name="no_saved_status">Du hast keine Entwürfe.</string> <string name="no_saved_status">Du hast keine Entwürfe.</string>
<string name="no_scheduled_status">Du hast keine geplanten Beiträge.</string> <string name="no_scheduled_status">Du hast keine geplanten Beiträge.</string>
<string name="warning_scheduling_interval">Das Datum des geplanten Toots muss mindestens 5 Minuten in der Zukunft liegen.</string>
</resources> </resources>

View File

@ -236,4 +236,27 @@
<string name="hint_search_people_list">Nadi γef medden i teṭafareḍ</string> <string name="hint_search_people_list">Nadi γef medden i teṭafareḍ</string>
<string name="description_visiblity_private">Imeḍfaṛen</string> <string name="description_visiblity_private">Imeḍfaṛen</string>
<string name="action_links">Iseγwan</string>
<string name="action_mentions">Tibdarin</string>
<string name="title_mentions_dialog">Tibdarin</string>
<string name="title_links_dialog">Iseγwan</string>
<string name="confirmation_reported">Yettwaceyyaɛ!</string>
<string name="status_sent">Yettwaceyyaɛ!</string>
<string name="search_no_results">Ula d yiwen n ugmuḍ</string>
<string name="post_privacy_followers_only">I yimeḍfaṛen kan</string>
<string name="pref_status_text_size">Teγzi n weḍṛis</string>
<string name="about_powered_by_tusky">Yettwamdemmar s Tusky</string>
<string name="about_project_site">Asmel Web n usenfaṛ:
\n https://tusky.app</string>
<string name="abbreviated_hours_ago">%dasr</string>
<string name="abbreviated_minutes_ago">%dtas</string>
<string name="abbreviated_seconds_ago">%dtasn</string>
<string name="compose_save_draft">Sekles amzun d arewway\?</string>
<string name="later">Ticki</string>
<string name="profile_badge_bot_text">Aṛubut</string>
<string name="description_status_bookmarked">Yettwarna γer ticṛad</string>
</resources> </resources>

View File

@ -10,7 +10,7 @@
<color name="textColorPrimary">@color/white</color> <color name="textColorPrimary">@color/white</color>
<color name="textColorSecondary">@color/tusky_grey_90</color> <color name="textColorSecondary">@color/tusky_grey_90</color>
<color name="textColorTertiary">@color/tusky_grey_70</color> <color name="textColorTertiary">@color/tusky_grey_70</color>
<color name="textColorDisabled">@color/tusky_grey_30</color> <color name="textColorDisabled">@color/tusky_grey_40</color>
<color name="iconColor">@color/tusky_grey_70</color> <color name="iconColor">@color/tusky_grey_70</color>

View File

@ -337,8 +337,8 @@
<string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string>
<plurals name="favs"> <plurals name="favs">
<item quantity="one">&lt;b&gt;%1$s&lt;/b&gt; Favorit</item> <item quantity="one"><b>%1$s</b> Favorit</item>
<item quantity="other">&lt;b&gt;3%1$s&lt;/b&gt;4 Favorits</item> <item quantity="other"><b>%1$s</b> Favorits</item>
</plurals> </plurals>
<plurals name="reblogs"> <plurals name="reblogs">

View File

@ -32,7 +32,7 @@
<string name="title_favourites">Favoritos</string> <string name="title_favourites">Favoritos</string>
<string name="title_mutes">Usuários silenciados</string> <string name="title_mutes">Usuários silenciados</string>
<string name="title_blocks">Usuários bloqueados</string> <string name="title_blocks">Usuários bloqueados</string>
<string name="title_follow_requests">Solicitações de seguidor</string> <string name="title_follow_requests">Seguidores pendentes</string>
<string name="title_edit_profile">Editar seu perfil</string> <string name="title_edit_profile">Editar seu perfil</string>
<string name="title_saved_toot">Rascunhos</string> <string name="title_saved_toot">Rascunhos</string>
<string name="title_licenses">Licenças</string> <string name="title_licenses">Licenças</string>
@ -76,7 +76,7 @@
<string name="action_view_favourites">Favoritos</string> <string name="action_view_favourites">Favoritos</string>
<string name="action_view_mutes">Usuários silenciados</string> <string name="action_view_mutes">Usuários silenciados</string>
<string name="action_view_blocks">Usuários bloqueados</string> <string name="action_view_blocks">Usuários bloqueados</string>
<string name="action_view_follow_requests">Solicitações de seguidor</string> <string name="action_view_follow_requests">Seguidores pendentes</string>
<string name="action_view_media">Mídia</string> <string name="action_view_media">Mídia</string>
<string name="action_open_in_web">Abrir no navegador</string> <string name="action_open_in_web">Abrir no navegador</string>
<string name="action_add_media">Adicionar mídia</string> <string name="action_add_media">Adicionar mídia</string>

View File

@ -112,4 +112,19 @@
<string name="compose_save_draft">Uložiť koncept\?</string> <string name="compose_save_draft">Uložiť koncept\?</string>
<string name="send_toot_notification_channel_name">Odosielanie tootov</string> <string name="send_toot_notification_channel_name">Odosielanie tootov</string>
<string name="send_toot_notification_cancel_title">Odosielanie bolo zrušené</string> <string name="send_toot_notification_cancel_title">Odosielanie bolo zrušené</string>
<string name="performing_lookup_title">Vyhľadávanie…</string>
<string name="later">Neskôr</string>
<string name="restart">Reštartovať</string>
<string name="profile_badge_bot_text">Robot</string>
<string name="license_cc_by_4">CC-BY 4.0</string>
<string name="license_cc_by_sa_4">CC-BY-SA 4.0</string>
<string name="profile_metadata_label">Metadáta profilu</string>
<string name="profile_metadata_add">pridať dáta</string>
<string name="profile_metadata_content_label">Obsah</string>
<string name="conversation_1_recipients">%1$s</string>
<string name="conversation_2_recipients">%1$s a %2$s</string>
<string name="description_status_media_no_description_placeholder">Žiadny popis</string>
<string name="description_visiblity_public">Verejný</string>
</resources> </resources>

View File

@ -4,7 +4,7 @@
<string name="error_network">Ett nätverksfel inträffade! Kontrollera att du är ansluten till internet och försök igen! </string> <string name="error_network">Ett nätverksfel inträffade! Kontrollera att du är ansluten till internet och försök igen! </string>
<string name="error_empty">Det här kan inte vara tomt.</string> <string name="error_empty">Det här kan inte vara tomt.</string>
<string name="error_invalid_domain">Ogiltig domän angiven</string> <string name="error_invalid_domain">Ogiltig domän angiven</string>
<string name="error_failed_app_registration">Misslyckades med att autentisera med den instansen.</string> <string name="error_failed_app_registration">Kunde inte med att autentisera med den instansen.</string>
<string name="error_no_web_browser_found">Det gick inte att hitta en webbläsare.</string> <string name="error_no_web_browser_found">Det gick inte att hitta en webbläsare.</string>
<string name="error_authorization_unknown">Ett oidentifierat behörighetsfel inträffade.</string> <string name="error_authorization_unknown">Ett oidentifierat behörighetsfel inträffade.</string>
<string name="error_authorization_denied">Ingen behörighet.</string> <string name="error_authorization_denied">Ingen behörighet.</string>
@ -42,7 +42,7 @@
<string name="status_boosted_format">%s knuffade</string> <string name="status_boosted_format">%s knuffade</string>
<string name="status_sensitive_media_title">Känsligt innehåll</string> <string name="status_sensitive_media_title">Känsligt innehåll</string>
<string name="status_media_hidden_title">Dold media</string> <string name="status_media_hidden_title">Dold media</string>
<string name="status_sensitive_media_directions">Klicka för att se</string> <string name="status_sensitive_media_directions">Tryck för att visa</string>
<string name="status_content_warning_show_more">Visa mer</string> <string name="status_content_warning_show_more">Visa mer</string>
<string name="status_content_warning_show_less">Visa mindre</string> <string name="status_content_warning_show_less">Visa mindre</string>
<string name="status_content_show_more">Expandera</string> <string name="status_content_show_more">Expandera</string>
@ -152,7 +152,7 @@
<string name="dialog_message_uploading_media">Laddar upp…</string> <string name="dialog_message_uploading_media">Laddar upp…</string>
<string name="dialog_download_image">Ladda ned</string> <string name="dialog_download_image">Ladda ned</string>
<string name="dialog_message_cancel_follow_request">Återkalla följningsförfrågan?</string> <string name="dialog_message_cancel_follow_request">Återkalla följningsförfrågan?</string>
<string name="dialog_unfollow_warning">Avfölja detta konto?</string> <string name="dialog_unfollow_warning">Sluta följ detta konto\?</string>
<string name="dialog_delete_toot_warning">Radera denna toot?</string> <string name="dialog_delete_toot_warning">Radera denna toot?</string>
<string name="visibility_public">Offentlig: Skicka till offentliga tidslinjer</string> <string name="visibility_public">Offentlig: Skicka till offentliga tidslinjer</string>
<string name="visibility_unlisted">Olistad: Visa inte i offentliga tidslinjer</string> <string name="visibility_unlisted">Olistad: Visa inte i offentliga tidslinjer</string>
@ -170,7 +170,7 @@
<string name="pref_title_notification_filter_reblogs">mina inlägg är knuffade</string> <string name="pref_title_notification_filter_reblogs">mina inlägg är knuffade</string>
<string name="pref_title_notification_filter_favourites">mina inlägg är favoriserade</string> <string name="pref_title_notification_filter_favourites">mina inlägg är favoriserade</string>
<string name="pref_title_appearance_settings">Utseende</string> <string name="pref_title_appearance_settings">Utseende</string>
<string name="pref_title_app_theme">Applikationstema</string> <string name="pref_title_app_theme">Tema</string>
<string name="pref_title_timelines">Tidslinjer</string> <string name="pref_title_timelines">Tidslinjer</string>
<string name="pref_title_timeline_filters">Filter</string> <string name="pref_title_timeline_filters">Filter</string>
<string name="app_them_dark">Mörkt</string> <string name="app_them_dark">Mörkt</string>
@ -395,8 +395,8 @@
<string name="notification_poll_description">Notifieringar när omröstningar har avslutats</string> <string name="notification_poll_description">Notifieringar när omröstningar har avslutats</string>
<string name="poll_ended_voted">En undersökning där du har röstat är avslutad</string> <string name="poll_ended_voted">En omröstning där du har röstat är avslutad</string>
<string name="poll_ended_created">En undersökning du skapat har avslutats</string> <string name="poll_ended_created">En omröstning som du har skapat har avslutats</string>
<plurals name="poll_timespan_days"> <plurals name="poll_timespan_days">
<item quantity="one">%d dag</item> <item quantity="one">%d dag</item>
@ -419,7 +419,7 @@
<string name="pref_title_animate_gif_avatars">Animera profil gifar</string> <string name="pref_title_animate_gif_avatars">Animera profil gifar</string>
<string name="description_poll">Undersökning med valen: %1$s, %2$s, %3$s, %4$s; %5$s</string> <string name="description_poll">Omröstning med valen: %1$s, %2$s, %3$s, %4$s; %5$s</string>
<string name="title_domain_mutes">Dolda domäner</string> <string name="title_domain_mutes">Dolda domäner</string>
<string name="action_view_domain_mutes">Dolda domäner</string> <string name="action_view_domain_mutes">Dolda domäner</string>
@ -438,7 +438,7 @@
<string name="report_remote_instance">Vidarebefordra till %s</string> <string name="report_remote_instance">Vidarebefordra till %s</string>
<string name="failed_report">Misslyckades att anmäla</string> <string name="failed_report">Misslyckades att anmäla</string>
<string name="failed_fetch_statuses">Misslyckades att hämta status</string> <string name="failed_fetch_statuses">Misslyckades att hämta status</string>
<string name="report_description_1">Anmälan kommer att skickas till din serveradminstratör. Du kan beskriva varför du anmäler kontot nedan:</string> <string name="report_description_1">Anmälan kommer att skickas till din servermoderator. Du kan beskriva varför du anmäler kontot nedan:</string>
<string name="report_description_remote_instance">Kontot är från en annan server. Skicka en anonym kopia av anmälan dit också\?</string> <string name="report_description_remote_instance">Kontot är från en annan server. Skicka en anonym kopia av anmälan dit också\?</string>
<string name="pref_title_show_notifications_filter">Visa notifikationsfilter</string> <string name="pref_title_show_notifications_filter">Visa notifikationsfilter</string>
@ -448,7 +448,7 @@
<string name="title_accounts">Konton</string> <string name="title_accounts">Konton</string>
<string name="failed_search">Sökning misslyckades</string> <string name="failed_search">Sökning misslyckades</string>
<string name="action_add_poll">Lägg till omröstning</string> <string name="action_add_poll">Skapa en omröstning</string>
<string name="create_poll_title">Omröstning</string> <string name="create_poll_title">Omröstning</string>
<string name="poll_duration_5_min">5 minuter</string> <string name="poll_duration_5_min">5 minuter</string>
<string name="poll_duration_30_min">30 minuter</string> <string name="poll_duration_30_min">30 minuter</string>
@ -463,7 +463,7 @@
<string name="edit_poll">Redigera</string> <string name="edit_poll">Redigera</string>
<string name="title_scheduled_toot">Schemalagda toots</string> <string name="title_scheduled_toot">Schemalagda toots</string>
<string name="action_edit">Redigera</string> <string name="action_edit">Ändra</string>
<string name="action_access_scheduled_toot">Schemalagda toots</string> <string name="action_access_scheduled_toot">Schemalagda toots</string>
<string name="action_schedule_toot">Schemalägg toot</string> <string name="action_schedule_toot">Schemalägg toot</string>
<string name="action_reset_schedule">Återställ</string> <string name="action_reset_schedule">Återställ</string>

View File

@ -575,5 +575,6 @@
<string name="no_saved_status">You don\'t have any drafts.</string> <string name="no_saved_status">You don\'t have any drafts.</string>
<string name="no_scheduled_status">You don\'t have any scheduled statuses.</string> <string name="no_scheduled_status">You don\'t have any scheduled statuses.</string>
<string name="warning_scheduling_interval">Mastodon has a minimum scheduling interval of 5 minutes.</string>
</resources> </resources>

View File

@ -119,10 +119,21 @@ class BottomSheetActivityTest {
arrayOf("https://mastodon.foo.bar/@user/345667890345678", true), arrayOf("https://mastodon.foo.bar/@user/345667890345678", true),
arrayOf("https://mastodon.foo.bar/@user/3", true), arrayOf("https://mastodon.foo.bar/@user/3", true),
arrayOf("https://pleroma.foo.bar/users/meh3223", true), arrayOf("https://pleroma.foo.bar/users/meh3223", true),
arrayOf("https://pleroma.foo.bar/users/meh3223_bruh", true),
arrayOf("https://pleroma.foo.bar/users/2345", true), arrayOf("https://pleroma.foo.bar/users/2345", true),
arrayOf("https://pleroma.foo.bar/notice/9", true), arrayOf("https://pleroma.foo.bar/notice/9", true),
arrayOf("https://pleroma.foo.bar/notice/9345678", true), arrayOf("https://pleroma.foo.bar/notice/9345678", true),
arrayOf("https://pleroma.foo.bar/notice/wat", true),
arrayOf("https://pleroma.foo.bar/notice/9qTHT2ANWUdXzENqC0", true),
arrayOf("https://pleroma.foo.bar/objects/abcdef-123-abcd-9876543", true), arrayOf("https://pleroma.foo.bar/objects/abcdef-123-abcd-9876543", true),
arrayOf("https://misskey.foo.bar/notes/mew", true),
arrayOf("https://misskey.foo.bar/notes/1421564653", true),
arrayOf("https://misskey.foo.bar/notes/qwer615985ddf", true),
arrayOf("https://friendica.foo.bar/profile/user", true),
arrayOf("https://friendica.foo.bar/profile/uSeR", true),
arrayOf("https://friendica.foo.bar/profile/user_user", true),
arrayOf("https://friendica.foo.bar/profile/123", true),
arrayOf("https://friendica.foo.bar/display/abcdef-123-abcd-9876543", true),
arrayOf("https://google.com/", false), arrayOf("https://google.com/", false),
arrayOf("https://mastodon.foo.bar/@User?foo=bar", false), arrayOf("https://mastodon.foo.bar/@User?foo=bar", false),
arrayOf("https://mastodon.foo.bar/@User#foo", false), arrayOf("https://mastodon.foo.bar/@User#foo", false),
@ -131,13 +142,23 @@ class BottomSheetActivityTest {
arrayOf("https://mastodon.foo.bar/@user/345667890345678/", false), arrayOf("https://mastodon.foo.bar/@user/345667890345678/", false),
arrayOf("https://mastodon.foo.bar/@user/3abce", false), arrayOf("https://mastodon.foo.bar/@user/3abce", false),
arrayOf("https://pleroma.foo.bar/users/", false), arrayOf("https://pleroma.foo.bar/users/", false),
arrayOf("https://pleroma.foo.bar/users/meow/", false),
arrayOf("https://pleroma.foo.bar/users/@meow", false),
arrayOf("https://pleroma.foo.bar/user/2345", false), arrayOf("https://pleroma.foo.bar/user/2345", false),
arrayOf("https://pleroma.foo.bar/notice/wat", false),
arrayOf("https://pleroma.foo.bar/notices/123456", false), arrayOf("https://pleroma.foo.bar/notices/123456", false),
arrayOf("https://pleroma.foo.bar/notice/@neverhappen/", false),
arrayOf("https://pleroma.foo.bar/object/abcdef-123-abcd-9876543", false), arrayOf("https://pleroma.foo.bar/object/abcdef-123-abcd-9876543", false),
arrayOf("https://pleroma.foo.bar/objects/xabcdef-123-abcd-9876543", false), arrayOf("https://pleroma.foo.bar/objects/xabcdef-123-abcd-9876543", false),
arrayOf("https://pleroma.foo.bar/objects/xabcdef-123-abcd-9876543/", false), arrayOf("https://pleroma.foo.bar/objects/xabcdef-123-abcd-9876543/", false),
arrayOf("https://pleroma.foo.bar/objects/xabcdef-123-abcd_9876543", false) arrayOf("https://pleroma.foo.bar/objects/xabcdef-123-abcd_9876543", false),
arrayOf("https://friendica.foo.bar/display/xabcdef-123-abcd-9876543", false),
arrayOf("https://friendica.foo.bar/display/xabcdef-123-abcd-9876543/", false),
arrayOf("https://friendica.foo.bar/display/xabcdef-123-abcd_9876543", false),
arrayOf("https://friendica.foo.bar/profile/@mew", false),
arrayOf("https://friendica.foo.bar/profile/@mew/", false),
arrayOf("https://misskey.foo.bar/notes/@nyan", false),
arrayOf("https://misskey.foo.bar/notes/NYAN123", false),
arrayOf("https://misskey.foo.bar/notes/meow123/", false)
) )
} }
} }
@ -299,4 +320,4 @@ class BottomSheetActivityTest {
this.fallbackBehavior = fallbackBehavior this.fallbackBehavior = fallbackBehavior
} }
} }
} }

View File

@ -47,7 +47,7 @@ import org.robolectric.fakes.RoboMenuItem
* Created by charlag on 3/7/18. * Created by charlag on 3/7/18.
*/ */
@Config(application = FakeTuskyApplication::class, sdk = [28]) @Config(sdk = [28])
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ComposeActivityTest { class ComposeActivityTest {
private lateinit var activity: ComposeActivity private lateinit var activity: ComposeActivity

View File

@ -1,26 +0,0 @@
package com.keylesspalace.tusky
/**
* Created by charlag on 3/7/18.
*/
class FakeTuskyApplication : TuskyApplication() {
private lateinit var locator: ServiceLocator
override fun initSecurityProvider() {
// No-op
}
override fun initAppInjector() {
// No-op
}
override fun initNightMode() {
// No-op
}
override fun getServiceLocator(): ServiceLocator {
return locator
}
}

View File

@ -24,7 +24,7 @@ import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
import java.util.* import java.util.*
@Config(application = FakeTuskyApplication::class, sdk = [28]) @Config(sdk = [28])
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class FilterTest { class FilterTest {

View File

@ -0,0 +1,51 @@
/* 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 <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky
import android.app.Application
import android.content.Context
import android.content.res.Configuration
import android.util.Log
import androidx.emoji.text.EmojiCompat
import com.keylesspalace.tusky.util.LocaleManager
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import de.c1710.filemojicompat.FileEmojiCompatConfig
import javax.inject.Inject
// override TuskyApplication for Robolectric tests, only initialize the necessary stuff
class TuskyApplication : Application() {
override fun onCreate() {
super.onCreate()
EmojiCompat.init(FileEmojiCompatConfig(this, ""))
}
override fun attachBaseContext(base: Context) {
localeManager = LocaleManager(base)
super.attachBaseContext(localeManager.setLocale(base))
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
localeManager.setLocale(this)
}
companion object {
@JvmStatic
lateinit var localeManager: LocaleManager
}
}

View File

@ -1,2 +0,0 @@
package com.keylesspalace.tusky.di

View File

@ -2,14 +2,13 @@ package com.keylesspalace.tusky.util
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.keylesspalace.tusky.FakeTuskyApplication
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
@Config(application = FakeTuskyApplication::class, sdk = [28]) @Config(sdk = [28])
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class SmartLengthInputFilterTest { class SmartLengthInputFilterTest {

View File

@ -0,0 +1,8 @@
Tusky v10.0
- Du kan nå legge til statuser som bokmerker, og se bokmerkene i Tusky.
- Du kan nå planlegge et toot for publisering i framtiden.
- Du kan nå legge til lister på hovedskjermen.
- Du kan nå publisere lydvedlegg med Tusky.
I tillegg er det mange andre mindre forbedringer og feilrettinger!

View File

@ -0,0 +1,8 @@
Tusky v10.0
- Pra quem não aguenta mais perder toots no meio dos favoritos, o Salvos chegou!
- Agora dá para agendar toots, porém é necessário agendá-los para ao menos 5 minutos depois, certo?
- Utilidade pública: Finalmente poderemos adicionar listas na barrinha do Tusky!
- Filosofou no áudio de uma conversa e quer compartilhar com o fediverso? Você já pode anexar áudios nos toots, só não se esqueça de descrevê-los!
E muitas outras pequenas melhorias e correções de bugs!

View File

@ -15,7 +15,7 @@ org.gradle.jvmargs=-Xmx4096m
# use parallel execution # use parallel execution
org.gradle.parallel=true org.gradle.parallel=true
android.enableJetifier=true
android.useAndroidX=true
android.enableUnitTestBinaryResources=true android.enableUnitTestBinaryResources=true
android.enableR8.fullMode=true android.enableR8.fullMode=true
android.enableJetifier=true
android.useAndroidX=true