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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,8 +22,8 @@ import android.view.ViewGroup
import androidx.core.app.ActivityOptionsCompat
import androidx.core.view.ViewCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.paging.PagedList
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration
@ -59,19 +59,13 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler {
@Inject
lateinit var accountManager: AccountManager
private lateinit var viewModel: ReportViewModel
private val viewModel: ReportViewModel by viewModels({ requireActivity() }) { viewModelFactory }
private lateinit var adapter: StatusesAdapter
private lateinit var layoutManager: LinearLayoutManager
private var snackbarErrorRetry: Snackbar? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProviders.of(requireActivity(), viewModelFactory)[ReportViewModel::class.java]
}
override fun showMedia(v: View?, status: Status?, idx: Int) {
status?.actionableStatus?.let { actionable ->
when (actionable.attachments[idx].type) {

View File

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

View File

@ -5,9 +5,9 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.paging.PagedList
import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DividerItemDecoration
@ -30,11 +30,12 @@ import javax.inject.Inject
abstract class SearchFragment<T> : Fragment(),
LinkListener, Injectable, SwipeRefreshLayout.OnRefreshListener {
private var snackbarErrorRetry: Snackbar? = null
@Inject
lateinit var viewModelFactory: ViewModelFactory
protected lateinit var viewModel: SearchViewModel
protected val viewModel: SearchViewModel by viewModels({ requireActivity() }) { viewModelFactory }
private var snackbarErrorRetry: Snackbar? = null
abstract fun createAdapter(): PagedListAdapter<T, *>
@ -43,11 +44,6 @@ abstract class SearchFragment<T> : Fragment(),
abstract val data: LiveData<PagedList<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? {
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 com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Status
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.Comparator
/**
* This class caches the account database and handles all account related operations
@ -26,7 +30,8 @@ import com.keylesspalace.tusky.entity.Status
private const val TAG = "AccountManager"
class AccountManager(db: AppDatabase) {
@Singleton
class AccountManager @Inject constructor(db: AppDatabase) {
@Volatile
var activeAccount: AccountEntity? = null
@ -60,7 +65,7 @@ class AccountManager(db: AppDatabase) {
val maxAccountId = accounts.maxBy { it.id }?.id ?: 0
val newAccountId = maxAccountId + 1
activeAccount = AccountEntity(id = newAccountId, domain = domain.toLowerCase(), accessToken = accessToken, isActive = true)
activeAccount = AccountEntity(id = newAccountId, domain = domain.toLowerCase(Locale.ROOT), accessToken = accessToken, isActive = true)
}
@ -146,8 +151,8 @@ class AccountManager(db: AppDatabase) {
saveAccount(it)
}
activeAccount = accounts.find { acc ->
acc.id == accountId
activeAccount = accounts.find { (id) ->
id == accountId
}
activeAccount?.let {
@ -185,8 +190,8 @@ class AccountManager(db: AppDatabase) {
* @return the requested account or null if it was not found
*/
fun getAccountById(accountId: Long): AccountEntity? {
return accounts.find { acc ->
acc.id == accountId
return accounts.find { (id) ->
id == accountId
}
}

View File

@ -21,10 +21,10 @@ import android.content.Context
import android.content.SharedPreferences
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.preference.PreferenceManager
import androidx.room.Room
import com.keylesspalace.tusky.TuskyApplication
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.EventHubImpl
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.network.TimelineCases
@ -64,20 +64,23 @@ class AppModule {
return TimelineCasesImpl(api, eventHub)
}
@Provides
@Singleton
fun providesAccountManager(app: TuskyApplication): AccountManager {
return app.serviceLocator.get(AccountManager::class.java)
}
@Provides
@Singleton
fun providesEventHub(): EventHub = EventHubImpl
@Provides
@Singleton
fun providesDatabase(app: TuskyApplication): AppDatabase {
return app.serviceLocator.get(AppDatabase::class.java)
fun providesDatabase(appContext: Context): AppDatabase {
return Room.databaseBuilder(appContext, AppDatabase::class.java, "tuskyDB")
.allowMainThreadQueries()
.addMigrations(AppDatabase.MIGRATION_2_3, AppDatabase.MIGRATION_3_4, AppDatabase.MIGRATION_4_5,
AppDatabase.MIGRATION_5_6, AppDatabase.MIGRATION_6_7, AppDatabase.MIGRATION_7_8,
AppDatabase.MIGRATION_8_9, AppDatabase.MIGRATION_9_10, AppDatabase.MIGRATION_10_11,
AppDatabase.MIGRATION_11_12, AppDatabase.MIGRATION_12_13, AppDatabase.MIGRATION_10_13,
AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16,
AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19,
AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21)
.build()
}
@Provides

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -39,7 +39,9 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<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
android:id="@+id/messageTextView"
@ -13,7 +26,6 @@
android:paddingRight="16dp"
android:textAlignment="center"
android:textSize="?attr/status_text_medium"
tools:drawableTop="@drawable/elephant_offline"
tools:text="@string/error_network" />
<Button
@ -22,5 +34,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="4dp"
android:text="@string/action_retry" />
</merge>

View File

@ -10,7 +10,7 @@
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/action_reset_schedule"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@id/invalidScheduleWarning"
app:layout_constraintStart_toStartOf="parent" />
<TextView
@ -23,10 +23,27 @@
android:paddingBottom="16dp"
android:textColor="?android:textColorTertiary"
android:textSize="?attr/status_text_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@id/invalidScheduleWarning"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintStart_toEndOf="@id/resetScheduleButton"
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>

View File

@ -469,5 +469,6 @@
<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="warning_scheduling_interval">Das Datum des geplanten Toots muss mindestens 5 Minuten in der Zukunft liegen.</string>
</resources>

View File

@ -236,4 +236,27 @@
<string name="hint_search_people_list">Nadi γef medden i teṭafareḍ</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>

View File

@ -10,7 +10,7 @@
<color name="textColorPrimary">@color/white</color>
<color name="textColorSecondary">@color/tusky_grey_90</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>

View File

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

View File

@ -32,7 +32,7 @@
<string name="title_favourites">Favoritos</string>
<string name="title_mutes">Usuários silenciados</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_saved_toot">Rascunhos</string>
<string name="title_licenses">Licenças</string>
@ -76,7 +76,7 @@
<string name="action_view_favourites">Favoritos</string>
<string name="action_view_mutes">Usuários silenciados</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_open_in_web">Abrir no navegador</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="send_toot_notification_channel_name">Odosielanie tootov</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>

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_empty">Det här kan inte vara tomt.</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_authorization_unknown">Ett oidentifierat behörighetsfel inträffade.</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_sensitive_media_title">Känsligt innehåll</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_less">Visa mindre</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_download_image">Ladda ned</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="visibility_public">Offentlig: Skicka till 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_favourites">mina inlägg är favoriserade</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_timeline_filters">Filter</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="poll_ended_voted">En undersökning 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_voted">En omröstning där du har röstat är avslutad</string>
<string name="poll_ended_created">En omröstning som du har skapat har avslutats</string>
<plurals name="poll_timespan_days">
<item quantity="one">%d dag</item>
@ -419,7 +419,7 @@
<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="action_view_domain_mutes">Dolda domäner</string>
@ -438,7 +438,7 @@
<string name="report_remote_instance">Vidarebefordra till %s</string>
<string name="failed_report">Misslyckades att anmäla</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="pref_title_show_notifications_filter">Visa notifikationsfilter</string>
@ -448,7 +448,7 @@
<string name="title_accounts">Konton</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="poll_duration_5_min">5 minuter</string>
<string name="poll_duration_30_min">30 minuter</string>
@ -463,7 +463,7 @@
<string name="edit_poll">Redigera</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_schedule_toot">Schemalägg toot</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_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>

View File

@ -119,10 +119,21 @@ class BottomSheetActivityTest {
arrayOf("https://mastodon.foo.bar/@user/345667890345678", true),
arrayOf("https://mastodon.foo.bar/@user/3", 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/notice/9", 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://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://mastodon.foo.bar/@User?foo=bar", 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/3abce", 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/notice/wat", 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/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
}
}
}
}

View File

@ -47,7 +47,7 @@ import org.robolectric.fakes.RoboMenuItem
* Created by charlag on 3/7/18.
*/
@Config(application = FakeTuskyApplication::class, sdk = [28])
@Config(sdk = [28])
@RunWith(AndroidJUnit4::class)
class ComposeActivityTest {
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 java.util.*
@Config(application = FakeTuskyApplication::class, sdk = [28])
@Config(sdk = [28])
@RunWith(AndroidJUnit4::class)
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 androidx.test.ext.junit.runners.AndroidJUnit4
import com.keylesspalace.tusky.FakeTuskyApplication
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@Config(application = FakeTuskyApplication::class, sdk = [28])
@Config(sdk = [28])
@RunWith(AndroidJUnit4::class)
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
org.gradle.parallel=true
android.enableJetifier=true
android.useAndroidX=true
android.enableUnitTestBinaryResources=true
android.enableR8.fullMode=true
android.enableJetifier=true
android.useAndroidX=true