diff --git a/app/build.gradle b/app/build.gradle index 09564b26..19497d5d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,8 +20,8 @@ android { applicationId APP_ID minSdkVersion 21 targetSdkVersion 29 - versionCode 75 - versionName "12.1" + versionCode 76 + versionName "13.0 beta 1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true @@ -98,31 +98,31 @@ project.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { ext.lifecycleVersion = "2.2.0" ext.roomVersion = '2.2.5' ext.retrofitVersion = '2.9.0' -ext.okhttpVersion = '4.8.1' +ext.okhttpVersion = '4.9.0' ext.glideVersion = '4.11.0' -ext.daggerVersion = '2.28.3' -ext.materialdrawerVersion = '8.1.4' +ext.daggerVersion = '2.29.1' +ext.materialdrawerVersion = '8.1.8' // if libraries are changed here, they should also be changed in LicenseActivity dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation "androidx.core:core-ktx:1.3.1" + implementation "androidx.core:core-ktx:1.3.2" implementation "androidx.appcompat:appcompat:1.2.0" implementation "androidx.fragment:fragment-ktx:1.2.5" implementation "androidx.browser:browser:1.2.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.recyclerview:recyclerview:1.1.0" - implementation "androidx.exifinterface:exifinterface:1.2.0" + implementation "androidx.exifinterface:exifinterface:1.3.1" implementation "androidx.cardview:cardview:1.0.0" - implementation "androidx.preference:preference:1.1.1" + implementation "androidx.preference:preference-ktx:1.1.1" implementation "androidx.sharetarget:sharetarget:1.0.0" implementation "androidx.emoji:emoji:1.1.0" implementation "androidx.emoji:emoji-appcompat:1.1.0" 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.constraintlayout:constraintlayout:2.0.4" implementation "androidx.paging:paging-runtime-ktx:2.1.2" implementation "androidx.viewpager2:viewpager2:1.0.0" implementation "androidx.work:work-runtime:2.4.0" @@ -130,7 +130,7 @@ dependencies { implementation "androidx.room:room-rxjava2:$roomVersion" kapt "androidx.room:room-compiler:$roomVersion" - implementation "com.google.android.material:material:1.2.0" + implementation "com.google.android.material:material:1.2.1" implementation "com.squareup.retrofit2:retrofit:$retrofitVersion" implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion" @@ -144,7 +144,7 @@ dependencies { implementation "com.github.bumptech.glide:glide:$glideVersion" implementation "com.github.bumptech.glide:okhttp3-integration:$glideVersion" - implementation "io.reactivex.rxjava2:rxjava:2.2.19" + implementation "io.reactivex.rxjava2:rxjava:2.2.20" implementation "io.reactivex.rxjava2:rxandroid:2.1.1" implementation "io.reactivex.rxjava2:rxkotlin:2.4.0" @@ -169,14 +169,12 @@ dependencies { implementation "de.c1710:filemojicompat:1.0.17" - testImplementation "androidx.test.ext:junit:1.1.1" - testImplementation "org.robolectric:robolectric:4.3.1" - testImplementation "org.mockito:mockito-inline:3.3.3" + testImplementation "androidx.test.ext:junit:1.1.2" + testImplementation "org.robolectric:robolectric:4.4" + testImplementation "org.mockito:mockito-inline:3.6.0" testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" - androidTestImplementation "androidx.test.espresso:espresso-core:3.2.0" - androidTestImplementation "android.arch.persistence.room:testing:1.1.1" - androidTestImplementation "androidx.test.ext:junit:1.1.1" - - debugImplementation "im.dino:dbinspector:4.0.0@aar" + androidTestImplementation "androidx.test.espresso:espresso-core:3.3.0" + androidTestImplementation "androidx.room:room-testing:$roomVersion" + androidTestImplementation "androidx.test.ext:junit:1.1.2" } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index e25a4564..78b4be88 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -55,6 +55,9 @@ public static *** v(...); public static *** i(...); } +-assumenosideeffects class java.lang.String { + public static java.lang.String format(...); +} # remove some kotlin overhead -assumenosideeffects class kotlin.jvm.internal.Intrinsics { @@ -62,8 +65,3 @@ static void checkExpressionValueIsNotNull(java.lang.Object, java.lang.String); static void throwUninitializedPropertyAccessException(java.lang.String); } - -# without this emoji font downloading fails with AbstractMethodError --keep class * extends android.os.AsyncTask { - public *; -} diff --git a/app/src/androidTest/java/com/keylesspalace/tusky/TimelineDAOTest.kt b/app/src/androidTest/java/com/keylesspalace/tusky/TimelineDAOTest.kt index 241781b6..da55b08b 100644 --- a/app/src/androidTest/java/com/keylesspalace/tusky/TimelineDAOTest.kt +++ b/app/src/androidTest/java/com/keylesspalace/tusky/TimelineDAOTest.kt @@ -78,46 +78,39 @@ class TimelineDAOTest { fun cleanup() { val now = System.currentTimeMillis() val oldDate = now - TimelineRepository.CLEANUP_INTERVAL - 20_000 - val oldByThisAccount = makeStatus( + val oldThisAccount = makeStatus( statusId = 5, createdAt = oldDate ) - val oldByAnotherAccount = makeStatus( + val oldAnotherAccount = makeStatus( statusId = 10, createdAt = oldDate, - authorServerId = "100" + accountId = 2 ) - val oldForAnotherAccount = makeStatus( - accountId = 2, - statusId = 20, - authorServerId = "200", - createdAt = oldDate - ) - val recentByThisAccount = makeStatus( + val recentThisAccount = makeStatus( statusId = 30, createdAt = System.currentTimeMillis() ) - val recentByAnotherAccount = makeStatus( + val recentAnotherAccount = makeStatus( statusId = 60, createdAt = System.currentTimeMillis(), - authorServerId = "200" - ) + accountId = 2 + ) - for ((status, author, reblogAuthor) in listOf(oldByThisAccount, oldByAnotherAccount, - oldForAnotherAccount, recentByThisAccount, recentByAnotherAccount)) { + for ((status, author, reblogAuthor) in listOf(oldThisAccount, oldAnotherAccount, recentThisAccount, recentAnotherAccount)) { timelineDao.insertInTransaction(status, author, reblogAuthor) } - timelineDao.cleanup(1, "20", now - TimelineRepository.CLEANUP_INTERVAL) + timelineDao.cleanup(now - TimelineRepository.CLEANUP_INTERVAL) assertEquals( - listOf(recentByAnotherAccount, recentByThisAccount, oldByThisAccount), + listOf(recentThisAccount), timelineDao.getStatusesForAccount(1, null, null, 100).blockingGet() .map { it.toTriple() } ) assertEquals( - listOf(oldForAnotherAccount), + listOf(recentAnotherAccount), timelineDao.getStatusesForAccount(2, null, null, 100).blockingGet() .map { it.toTriple() } ) @@ -217,7 +210,8 @@ class TimelineDAOTest { application = "application$accountId", reblogServerId = if (reblog) (statusId * 100).toString() else null, reblogAccountId = reblogAuthor?.serverId, - poll = null + poll = null, + muted = false ) return Triple(status, author, reblogAuthor) } @@ -246,7 +240,8 @@ class TimelineDAOTest { application = null, reblogServerId = null, reblogAccountId = null, - poll = null + poll = null, + muted = false ) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 53564ace..585ff833 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -36,7 +36,7 @@ + android:configChanges="orientation|screenSize|keyboardHidden" /> @@ -105,7 +105,7 @@ + android:windowSoftInputMode="stateVisible|adjustResize" /> @@ -117,7 +117,7 @@ android:name=".AccountActivity" android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize" /> - + @@ -145,6 +145,7 @@ android:windowSoftInputMode="stateAlwaysHidden|adjustResize" /> + + tools:node="remove" /> - \ No newline at end of file + diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt index 09781875..478d8aff 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt @@ -23,6 +23,7 @@ import android.graphics.Color import android.graphics.PorterDuff import android.graphics.PorterDuffColorFilter import android.os.Bundle +import android.text.Editable import android.view.Menu import android.view.MenuItem import android.view.View @@ -34,7 +35,6 @@ import androidx.appcompat.app.AlertDialog import androidx.core.app.ActivityOptionsCompat import androidx.core.content.ContextCompat import androidx.emoji.text.EmojiCompat -import androidx.lifecycle.Observer import androidx.preference.PreferenceManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.viewpager2.widget.MarginPageTransformer @@ -132,6 +132,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI if (viewModel.isSelf) { updateButtons() + saveNoteInfo.hide() + } else { + saveNoteInfo.visibility = View.INVISIBLE } } @@ -311,7 +314,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI * Subscribe to data loaded at the view model */ private fun subscribeObservables() { - viewModel.accountData.observe(this, Observer { + viewModel.accountData.observe(this) { when (it) { is Success -> onAccountChanged(it.data) is Error -> { @@ -320,8 +323,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI .show() } } - }) - viewModel.relationshipData.observe(this, Observer { + } + viewModel.relationshipData.observe(this) { val relation = it?.data if (relation != null) { onRelationshipChanged(relation) @@ -333,12 +336,14 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI .show() } - }) - viewModel.accountFieldData.observe(this, Observer { + } + viewModel.accountFieldData.observe(this, { accountFieldAdapter.fields = it accountFieldAdapter.notifyDataSetChanged() - }) + viewModel.noteSaved.observe(this) { + saveNoteInfo.visible(it, View.INVISIBLE) + } } /** @@ -349,7 +354,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI viewModel.refresh() adapter.refreshContent() } - viewModel.isRefreshing.observe(this, Observer { isRefreshing -> + viewModel.isRefreshing.observe(this, { isRefreshing -> swipeToRefreshLayout.isRefreshing = isRefreshing == true }) swipeToRefreshLayout.setColorSchemeResources(R.color.tusky_blue) @@ -407,7 +412,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI accountAvatarImageView.setOnClickListener { avatarView -> - val intent = ViewMediaActivity.newAvatarIntent(avatarView.context, account.avatar) + val intent = ViewMediaActivity.newSingleImageIntent(avatarView.context, account.avatar) avatarView.transitionName = account.avatar val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this, avatarView, account.avatar) @@ -533,9 +538,22 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI accountFollowsYouTextView.visible(relation.followedBy) + accountNoteTextInputLayout.visible(relation.note != null) + accountNoteTextInputLayout.editText?.setText(relation.note) + + // add the listener late to avoid it firing on the first change + accountNoteTextInputLayout.editText?.removeTextChangedListener(noteWatcher) + accountNoteTextInputLayout.editText?.addTextChangedListener(noteWatcher) + updateButtons() } + private val noteWatcher = object: DefaultTextWatcher() { + override fun afterTextChanged(s: Editable) { + viewModel.noteChanged(s.toString()) + } + } + private fun updateFollowButton() { if (viewModel.isSelf) { accountFollowButton.setText(R.string.action_edit_own_profile) @@ -705,9 +723,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI loadedAccount?.let { showMuteAccountDialog( this, - it.username, - { notifications -> viewModel.muteAccount(notifications) } - ) + it.username + ) { notifications -> + viewModel.muteAccount(notifications) + } } } else { viewModel.unmuteAccount() diff --git a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt index 22d25f09..3d86b6e1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt @@ -30,8 +30,8 @@ import android.widget.ImageView import androidx.activity.viewModels import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat +import androidx.core.view.isVisible import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager import com.bumptech.glide.Glide import com.bumptech.glide.load.resource.bitmap.FitCenter @@ -40,8 +40,6 @@ import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.adapter.AccountFieldEditAdapter import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory -import com.keylesspalace.tusky.entity.Account -import com.keylesspalace.tusky.entity.Instance import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.viewmodel.EditProfileViewModel import com.mikepenz.iconics.IconicsDrawable @@ -112,7 +110,7 @@ class EditProfileActivity : BaseActivity(), Injectable { addFieldButton.setOnClickListener { accountFieldEditAdapter.addField() if(accountFieldEditAdapter.itemCount >= MAX_ACCOUNT_FIELDS) { - it.isEnabled = false + it.isVisible = false } scrollView.post{ @@ -122,7 +120,7 @@ class EditProfileActivity : BaseActivity(), Injectable { viewModel.obtainProfile() - viewModel.profileData.observe(this, Observer> { profileRes -> + viewModel.profileData.observe(this) { profileRes -> when (profileRes) { is Success -> { val me = profileRes.data @@ -163,10 +161,10 @@ class EditProfileActivity : BaseActivity(), Injectable { } } - }) + } viewModel.obtainInstance() - viewModel.instanceData.observe(this, Observer> { result -> + viewModel.instanceData.observe(this) { result -> when (result) { is Success -> { val instance = result.data @@ -175,12 +173,12 @@ class EditProfileActivity : BaseActivity(), Injectable { } } } - }) + } observeImage(viewModel.avatarData, avatarPreview, avatarProgressBar, true) observeImage(viewModel.headerData, headerPreview, headerProgressBar, false) - viewModel.saveData.observe(this, Observer> { + viewModel.saveData.observe(this, { when(it) { is Success -> { finish() @@ -215,7 +213,7 @@ class EditProfileActivity : BaseActivity(), Injectable { imageView: ImageView, progressBar: View, roundedCorners: Boolean) { - liveData.observe(this, Observer> { + liveData.observe(this, { when (it) { is Success -> { diff --git a/app/src/main/java/com/keylesspalace/tusky/EmojiPreference.java b/app/src/main/java/com/keylesspalace/tusky/EmojiPreference.java deleted file mode 100644 index 8837c7d8..00000000 --- a/app/src/main/java/com/keylesspalace/tusky/EmojiPreference.java +++ /dev/null @@ -1,291 +0,0 @@ -package com.keylesspalace.tusky; - -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.os.Build; -import android.util.AttributeSet; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.ProgressBar; -import android.widget.RadioButton; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.appcompat.app.AlertDialog; -import androidx.preference.Preference; -import androidx.preference.PreferenceManager; - -import com.keylesspalace.tusky.util.EmojiCompatFont; - -import java.util.ArrayList; - -/** - * This Preference lets the user select their preferred emoji font - */ -public class EmojiPreference extends Preference { - private static final String TAG = "EmojiPreference"; - private EmojiCompatFont selected, original; - static final String FONT_PREFERENCE = "selected_emoji_font"; - private static final EmojiCompatFont[] FONTS = EmojiCompatFont.FONTS; - // Please note that this array should be sorted in the same way as their fonts. - private static final int[] viewIds = { - R.id.item_nomoji, - R.id.item_blobmoji, - R.id.item_twemoji, - R.id.item_notoemoji}; - - private ArrayList radioButtons = new ArrayList<>(); - - private boolean updated, currentNeedsUpdate; - - public EmojiPreference(Context context) { - super(context); - - // Find out which font is currently active - this.selected = EmojiCompatFont.byId(PreferenceManager - .getDefaultSharedPreferences(context) - .getInt(FONT_PREFERENCE, 0)); - // We'll use this later to determine if anything has changed - this.original = this.selected; - - setSummary(selected.getDisplay(context)); - } - - public EmojiPreference(Context context, AttributeSet attrs) { - super(context, attrs); - - // Find out which font is currently active - this.selected = EmojiCompatFont.byId(PreferenceManager - .getDefaultSharedPreferences(context) - .getInt(FONT_PREFERENCE, 0)); - // We'll use this later to determine if anything has changed - this.original = this.selected; - - setSummary(selected.getDisplay(context)); - } - - @Override - protected void onClick() { - - View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_emojicompat, null); - - for (int i = 0; i < viewIds.length; i++) { - setupItem(view.findViewById(viewIds[i]), FONTS[i]); - } - - new AlertDialog.Builder(getContext()) - .setView(view) - .setPositiveButton(android.R.string.ok, (dialog, which) -> onDialogOk()) - .setNegativeButton(android.R.string.cancel, null) - .show(); - } - - private void setupItem(View container, EmojiCompatFont font) { - Context context = container.getContext(); - - TextView title = container.findViewById(R.id.emojicompat_name); - TextView caption = container.findViewById(R.id.emojicompat_caption); - ImageView thumb = container.findViewById(R.id.emojicompat_thumb); - ImageButton download = container.findViewById(R.id.emojicompat_download); - ImageButton cancel = container.findViewById(R.id.emojicompat_download_cancel); - RadioButton radio = container.findViewById(R.id.emojicompat_radio); - - // Initialize all the views - title.setText(font.getDisplay(context)); - caption.setText(font.getCaption(context)); - thumb.setImageDrawable(font.getThumb(context)); - - // There needs to be a list of all the radio buttons in order to uncheck them when one is selected - radioButtons.add(radio); - - updateItem(font, container); - - // Set actions - download.setOnClickListener((downloadButton) -> - startDownload(font, container)); - - cancel.setOnClickListener((cancelButton) -> - cancelDownload(font, container)); - - radio.setOnClickListener((radioButton) -> - select(font, (RadioButton) radioButton)); - - container.setOnClickListener((containterView) -> - select(font, - containterView.findViewById(R.id.emojicompat_radio - ))); - } - - private void startDownload(EmojiCompatFont font, View container) { - ImageButton download = container.findViewById(R.id.emojicompat_download); - TextView caption = container.findViewById(R.id.emojicompat_caption); - - ProgressBar progressBar = container.findViewById(R.id.emojicompat_progress); - ImageButton cancel = container.findViewById(R.id.emojicompat_download_cancel); - - // Switch to downloading style - download.setVisibility(View.GONE); - caption.setVisibility(View.INVISIBLE); - progressBar.setVisibility(View.VISIBLE); - cancel.setVisibility(View.VISIBLE); - - - font.downloadFont(getContext(), new EmojiCompatFont.Downloader.EmojiDownloadListener() { - @Override - public void onDownloaded(EmojiCompatFont font) { - finishDownload(font, container); - } - - @Override - public void onProgress(float progress) { - // The progress is returned as a float between 0 and 1 - progress *= progressBar.getMax(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - progressBar.setProgress((int) progress, true); - } else { - progressBar.setProgress((int) progress); - } - } - - @Override - public void onFailed() { - Toast.makeText(getContext(), R.string.download_failed, Toast.LENGTH_SHORT).show(); - updateItem(font, container); - } - }); - } - - private void cancelDownload(EmojiCompatFont font, View container) { - font.cancelDownload(); - updateItem(font, container); - } - - private void finishDownload(EmojiCompatFont font, View container) { - select(font, container.findViewById(R.id.emojicompat_radio)); - updateItem(font, container); - // Set the flag to restart the app (because an update has been downloaded) - if (selected == original && currentNeedsUpdate) { - updated = true; - currentNeedsUpdate = false; - } - } - - /** - * Select a font both visually and logically - * - * @param font The font to be selected - * @param radio The radio button associated with it's visual item - */ - private void select(EmojiCompatFont font, RadioButton radio) { - selected = font; - // Uncheck all the other buttons - for (RadioButton other : radioButtons) { - if (other != radio) { - other.setChecked(false); - } - } - radio.setChecked(true); - } - - /** - * Called when a "consistent" state is reached, i.e. it's not downloading the font - * - * @param font The font to be displayed - * @param container The ConstraintLayout containing the item - */ - private void updateItem(EmojiCompatFont font, View container) { - // Assignments - ImageButton download = container.findViewById(R.id.emojicompat_download); - TextView caption = container.findViewById(R.id.emojicompat_caption); - - ProgressBar progress = container.findViewById(R.id.emojicompat_progress); - ImageButton cancel = container.findViewById(R.id.emojicompat_download_cancel); - - RadioButton radio = container.findViewById(R.id.emojicompat_radio); - - // There's no download going on - progress.setVisibility(View.GONE); - cancel.setVisibility(View.GONE); - caption.setVisibility(View.VISIBLE); - - if (font.isDownloaded(getContext())) { - // Make it selectable - download.setVisibility(View.GONE); - radio.setVisibility(View.VISIBLE); - container.setClickable(true); - } else { - // Make it downloadable - download.setVisibility(View.VISIBLE); - radio.setVisibility(View.GONE); - container.setClickable(false); - } - - // Select it if necessary - if (font == selected) { - radio.setChecked(true); - // Update available - if (!font.isDownloaded(getContext())) { - currentNeedsUpdate = true; - } - } else { - radio.setChecked(false); - } - } - - - /** - * In order to be able to use this font later on, it needs to be saved first. - */ - private void saveSelectedFont() { - int index = selected.getId(); - Log.i(TAG, "saveSelectedFont: Font ID: " + index); - // It's saved using the key FONT_PREFERENCE - PreferenceManager - .getDefaultSharedPreferences(getContext()) - .edit() - .putInt(FONT_PREFERENCE, index) - .apply(); - setSummary(selected.getDisplay(getContext())); - } - - /** - * That's it. The user doesn't want to switch between these amazing radio buttons anymore! - * That means, the selected font can be saved (if the user hit OK) - */ - private void onDialogOk() { - saveSelectedFont(); - if (selected != original || updated) { - new AlertDialog.Builder(getContext()) - .setTitle(R.string.restart_required) - .setMessage(R.string.restart_emoji) - .setNegativeButton(R.string.later, null) - .setPositiveButton(R.string.restart, ((dialog, which) -> { - // Restart the app - // From https://stackoverflow.com/a/17166729/5070653 - Intent launchIntent = new Intent(getContext(), SplashActivity.class); - PendingIntent mPendingIntent = PendingIntent.getActivity( - getContext(), - // This is the codepoint of the party face emoji :D - 0x1f973, - launchIntent, - PendingIntent.FLAG_CANCEL_CURRENT); - AlarmManager mgr = - (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE); - if (mgr != null) { - mgr.set( - AlarmManager.RTC, - System.currentTimeMillis() + 100, - mPendingIntent); - } - System.exit(0); - })).show(); - } - - } - -} diff --git a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt index 163d9166..3894f652 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt @@ -41,10 +41,8 @@ import com.keylesspalace.tusky.viewmodel.ListsViewModel.Event.* import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.* import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial -import com.mikepenz.iconics.utils.color import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizeDp -import com.mikepenz.iconics.utils.toIconicsColor import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from import com.uber.autodispose.autoDispose import dagger.android.DispatchingAndroidInjector diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index 32222251..dc66945d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -19,7 +19,6 @@ import android.content.Context import android.content.DialogInterface import android.content.Intent import android.content.res.ColorStateList -import android.content.res.Configuration import android.graphics.Color import android.graphics.drawable.Drawable import android.net.Uri @@ -29,7 +28,6 @@ import android.view.KeyEvent import android.view.MenuItem import android.view.View import android.widget.ImageView -import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.AlertDialog import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.ContextCompat @@ -40,16 +38,22 @@ import androidx.lifecycle.Lifecycle import androidx.preference.PreferenceManager import androidx.viewpager2.widget.MarginPageTransformer import com.bumptech.glide.Glide +import com.bumptech.glide.RequestManager +import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.target.FixedSizeDrawable +import com.bumptech.glide.request.transition.Transition import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout.OnTabSelectedListener import com.google.android.material.tabs.TabLayoutMediator -import com.google.android.material.tabs.TabLayoutMediator.TabConfigurationStrategy import com.keylesspalace.tusky.appstore.* +import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType import com.keylesspalace.tusky.components.conversation.ConversationsRepository import com.keylesspalace.tusky.components.notifications.NotificationHelper +import com.keylesspalace.tusky.components.preference.PreferencesActivity import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity import com.keylesspalace.tusky.components.search.SearchActivity import com.keylesspalace.tusky.db.AccountEntity @@ -65,6 +69,9 @@ import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizeDp +import com.mikepenz.materialdrawer.holder.BadgeStyle +import com.mikepenz.materialdrawer.holder.ColorHolder +import com.mikepenz.materialdrawer.holder.StringHolder import com.mikepenz.materialdrawer.iconics.iconicsIcon import com.mikepenz.materialdrawer.model.* import com.mikepenz.materialdrawer.model.interfaces.* @@ -74,6 +81,7 @@ import com.uber.autodispose.android.lifecycle.autoDispose import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers import kotlinx.android.synthetic.main.activity_main.* import javax.inject.Inject @@ -91,13 +99,16 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje lateinit var conversationRepository: ConversationsRepository private lateinit var header: AccountHeaderView - private lateinit var drawerToggle: ActionBarDrawerToggle private var notificationTabPosition = 0 private var onTabSelectedListener: OnTabSelectedListener? = null + private var unreadAnnouncementsCount = 0 + private val preferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) } + private lateinit var glide: RequestManager + private val emojiInitCallback = object : InitCallback() { override fun onInitialized() { if (!isDestroyed) { @@ -108,7 +119,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (accountManager.activeAccount == null) { + + val activeAccount = accountManager.activeAccount + if (activeAccount == null) { // will be redirected to LoginActivity by BaseActivity return } @@ -161,6 +174,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } window.statusBarColor = Color.TRANSPARENT // don't draw a status bar, the DrawerLayout and the MaterialDrawerLayout have their own setContentView(R.layout.activity_main) + + glide = Glide.with(this) + composeButton.setOnClickListener { val composeIntent = Intent(applicationContext, ComposeActivity::class.java) startActivity(composeIntent) @@ -169,6 +185,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje val hideTopToolbar = preferences.getBoolean(PrefKeys.HIDE_TOP_TOOLBAR, false) mainToolbar.visible(!hideTopToolbar) + loadDrawerAvatar(activeAccount.profilePictureUrl, true) + mainToolbar.menu.add(R.string.action_search).apply { setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM) icon = IconicsDrawable(this@MainActivity, GoogleMaterial.Icon.gmd_search).apply { @@ -187,6 +205,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje * drawer, though, because its callback touches the header in the drawer. */ fetchUserInfo() + fetchAnnouncements() + setupTabs(showNotificationTab) // Setup push notifications @@ -202,11 +222,17 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje when (event) { is ProfileEditedEvent -> onFetchUserInfoSuccess(event.newProfileData) is MainTabsChangedEvent -> setupTabs(false) + is AnnouncementReadEvent -> { + unreadAnnouncementsCount-- + updateAnnouncementsBadge() + } } } - // Flush old media that was cached for sharing - deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Tusky")) + Schedulers.io().scheduleDirect { + // Flush old media that was cached for sharing + deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Tusky")) + } } override fun onResume() { @@ -261,8 +287,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje public override fun onPostCreate(savedInstanceState: Bundle?) { super.onPostCreate(savedInstanceState) - drawerToggle.syncState() - if (intent != null) { val statusUrl = intent.getStringExtra(STATUS_URL) if (statusUrl != null) { @@ -288,7 +312,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje private fun setupDrawer(savedInstanceState: Bundle?, addSearchButton: Boolean) { - drawerToggle = ActionBarDrawerToggle(this, mainDrawerLayout, mainToolbar, com.mikepenz.materialdrawer.R.string.material_drawer_open, com.mikepenz.materialdrawer.R.string.material_drawer_close) + mainToolbar.setNavigationOnClickListener { + mainDrawerLayout.open() + } header = AccountHeaderView(this).apply { headerBackgroundScaleType = ImageView.ScaleType.CENTER_CROP @@ -312,13 +338,11 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje DrawerImageLoader.init(object : AbstractDrawerImageLoader() { override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) { if (animateAvatars) { - Glide.with(imageView.context) - .load(uri) + glide.load(uri) .placeholder(placeholder) .into(imageView) } else { - Glide.with(imageView.context) - .asBitmap() + glide.asBitmap() .load(uri) .placeholder(placeholder) .into(imageView) @@ -326,7 +350,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } override fun cancel(imageView: ImageView) { - Glide.with(imageView.context).clear(imageView) + glide.clear(imageView) } override fun placeholder(ctx: Context, tag: String?): Drawable { @@ -388,6 +412,18 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje startActivityWithSlideInAnimation(ScheduledTootActivity.newIntent(context)) } }, + primaryDrawerItem { + identifier = DRAWER_ITEM_ANNOUNCEMENTS + nameRes = R.string.title_announcements + iconRes = R.drawable.ic_bullhorn_24dp + onClick = { + startActivityWithSlideInAnimation(AnnouncementsActivity.newIntent(context)) + } + badgeStyle = BadgeStyle().apply { + textColor = ColorHolder.fromColor(ThemeUtils.getColor(this@MainActivity, R.attr.colorOnPrimary)) + color = ColorHolder.fromColor(ThemeUtils.getColor(this@MainActivity, R.attr.colorPrimary)) + } + }, DividerDrawerItem(), secondaryDrawerItem { nameRes = R.string.action_view_account_preferences @@ -420,7 +456,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } ) - if(addSearchButton) { + if (addSearchButton) { mainDrawer.addItemsAtPosition(4, primaryDrawerItem { nameRes = R.string.action_search @@ -446,25 +482,13 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje EmojiCompat.get().registerInitCallback(emojiInitCallback) } - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - drawerToggle.onConfigurationChanged(newConfig) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (drawerToggle.onOptionsItemSelected(item)) { - return true - } - return super.onOptionsItemSelected(item) - } - override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(mainDrawer.saveInstanceState(outState)) } private fun setupTabs(selectNotificationTab: Boolean) { - val activeTabLayout = if(preferences.getString("mainNavPosition", "top") == "bottom") { + val activeTabLayout = if (preferences.getString("mainNavPosition", "top") == "bottom") { val actionBarSize = ThemeUtils.getDimension(this, R.attr.actionBarSize) val fabMargin = resources.getDimensionPixelSize(R.dimen.fabMargin) (composeButton.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = actionBarSize + fabMargin @@ -481,7 +505,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje val adapter = MainPagerAdapter(tabs, this) viewPager.adapter = adapter - TabLayoutMediator(activeTabLayout, viewPager, TabConfigurationStrategy { _: TabLayout.Tab?, _: Int -> }).attach() + TabLayoutMediator(activeTabLayout, viewPager) { _: TabLayout.Tab?, _: Int -> }.attach() activeTabLayout.removeAllTabs() for (i in tabs.indices) { val tab = activeTabLayout.newTab() @@ -614,11 +638,12 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } private fun onFetchUserInfoSuccess(me: Account) { - Glide.with(this) - .asBitmap() + glide.asBitmap() .load(me.header) .into(header.accountHeaderBackground) + loadDrawerAvatar(me.avatar, false) + accountManager.updateActiveAccount(me) NotificationHelper.createNotificationChannelsForAccount(accountManager.activeAccount!!, this) @@ -642,6 +667,55 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje updateShortcut(this, accountManager.activeAccount!!) } + private fun loadDrawerAvatar(avatarUrl: String, showPlaceholder: Boolean) { + val navIconSize = resources.getDimensionPixelSize(R.dimen.avatar_toolbar_nav_icon_size) + + glide.asDrawable() + .load(avatarUrl) + .transform( + RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp)) + ) + .apply { + if (showPlaceholder) { + placeholder(R.drawable.avatar_default) + } + } + .into(object : CustomTarget(navIconSize, navIconSize) { + + override fun onLoadStarted(placeholder: Drawable?) { + if(placeholder != null) { + mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize) + } + } + override fun onResourceReady(resource: Drawable, transition: Transition?) { + mainToolbar.navigationIcon = resource + } + + override fun onLoadCleared(placeholder: Drawable?) { + mainToolbar.navigationIcon = placeholder + } + }) + } + + private fun fetchAnnouncements() { + mastodonApi.listAnnouncements(false) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe( + { announcements -> + unreadAnnouncementsCount = announcements.count { !it.read } + updateAnnouncementsBadge() + }, + { + Log.w(TAG, "Failed to fetch announcements.", it) + } + ) + } + + private fun updateAnnouncementsBadge() { + mainDrawer.updateBadge(DRAWER_ITEM_ANNOUNCEMENTS, StringHolder(if (unreadAnnouncementsCount == 0) null else unreadAnnouncementsCount.toString())) + } + private fun updateProfiles() { val profiles: MutableList = accountManager.getAllAccountsOrderedByActive().map { acc -> val emojifiedName = EmojiCompat.get().process(acc.displayName.emojify(acc.emojis, header)) @@ -676,6 +750,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje private const val TAG = "MainActivity" // logging tag private const val DRAWER_ITEM_ADD_ACCOUNT: Long = -13 private const val DRAWER_ITEM_FOLLOW_REQUESTS: Long = 10 + private const val DRAWER_ITEM_ANNOUNCEMENTS: Long = 14 const val STATUS_URL = "statusUrl" } } @@ -705,4 +780,4 @@ private var AbstractDrawerItem<*, *>.onClick: () -> Unit value() false } - } \ No newline at end of file + } diff --git a/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java b/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java index 21cc9399..9a163989 100644 --- a/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java @@ -164,6 +164,7 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd List descriptions = gson.fromJson(item.getDescriptions(), stringListType); ComposeOptions composeOptions = new ComposeOptions( + /*scheduledTootUid*/null, item.getUid(), item.getText(), jsonUrls, @@ -178,7 +179,8 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd /*mediaAttachments*/null, /*scheduledAt*/null, /*sensitive*/null, - /*poll*/null + /*poll*/null, + /* modifiedInitialState */ true ); Intent intent = ComposeActivity.startIntent(this, composeOptions); startActivity(intent); diff --git a/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt index ad622ea7..6b1aaef9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt @@ -15,19 +15,25 @@ package com.keylesspalace.tusky +import android.graphics.Color import android.os.Bundle import android.util.Log import android.view.MenuItem +import android.view.View import android.widget.FrameLayout import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.AppCompatEditText +import androidx.core.view.isVisible import androidx.core.view.updatePadding import androidx.lifecycle.Lifecycle import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import androidx.transition.TransitionManager import at.connyduck.sparkbutton.helpers.Utils +import com.google.android.material.transition.MaterialArcMotion +import com.google.android.material.transition.MaterialContainerTransform import com.keylesspalace.tusky.adapter.ItemInteractionListener import com.keylesspalace.tusky.adapter.ListSelectionAdapter import com.keylesspalace.tusky.adapter.TabAdapter @@ -78,7 +84,7 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene setDisplayShowHomeEnabled(true) } - currentTabs = (accountManager.activeAccount?.tabPreferences ?: emptyList()).toMutableList() + currentTabs = accountManager.activeAccount?.tabPreferences.orEmpty().toMutableList() currentTabsAdapter = TabAdapter(currentTabs, false, this, currentTabs.size <= MIN_TAB_COUNT) currentTabsRecyclerView.adapter = currentTabsAdapter currentTabsRecyclerView.layoutManager = LinearLayoutManager(this) @@ -129,19 +135,17 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene touchHelper.attachToRecyclerView(currentTabsRecyclerView) - actionButton.setOnClickListener { - actionButton.isExpanded = true + toggleFab(true) } scrim.setOnClickListener { - actionButton.isExpanded = false + toggleFab(false) } maxTabsInfo.text = getString(R.string.max_tab_number_reached, MAX_TAB_COUNT) updateAvailableTabs() - } override fun onTabAdded(tab: TabData) { @@ -150,7 +154,7 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene return } - actionButton.isExpanded = false + toggleFab(false) if (tab.id == HASHTAG) { showAddHashtagDialog() @@ -175,20 +179,36 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene saveTabs() } - override fun onActionChipClicked(tab: TabData) { - showAddHashtagDialog(tab) + override fun onActionChipClicked(tab: TabData, tabPosition: Int) { + showAddHashtagDialog(tab, tabPosition) } - override fun onChipClicked(tab: TabData, chipPosition: Int) { + override fun onChipClicked(tab: TabData, tabPosition: Int, chipPosition: Int) { val newArguments = tab.arguments.filterIndexed { i, _ -> i != chipPosition } val newTab = tab.copy(arguments = newArguments) - val position = currentTabs.indexOf(tab) - currentTabs[position] = newTab + currentTabs[tabPosition] = newTab + saveTabs() - currentTabsAdapter.notifyItemChanged(position) + currentTabsAdapter.notifyItemChanged(tabPosition) } - private fun showAddHashtagDialog(tab: TabData? = null) { + private fun toggleFab(expand: Boolean) { + val transition = MaterialContainerTransform().apply { + startView = if (expand) actionButton else sheet + val endView: View = if (expand) sheet else actionButton + this.endView = endView + addTarget(endView) + scrimColor = Color.TRANSPARENT + setPathMotion(MaterialArcMotion()) + } + + TransitionManager.beginDelayedTransition(tabPreferenceContainer, transition) + actionButton.visible(!expand) + sheet.visible(expand) + scrim.visible(expand) + } + + private fun showAddHashtagDialog(tab: TabData? = null, tabPosition: Int = 0) { val frameLayout = FrameLayout(this) val padding = Utils.dpToPx(this, 8) @@ -211,10 +231,9 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene currentTabsAdapter.notifyItemInserted(currentTabs.size - 1) } else { val newTab = tab.copy(arguments = tab.arguments + input) - val position = currentTabs.indexOf(tab) - currentTabs[position] = newTab + currentTabs[tabPosition] = newTab - currentTabsAdapter.notifyItemChanged(position) + currentTabsAdapter.notifyItemChanged(tabPosition) } updateAvailableTabs() @@ -319,10 +338,10 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene } override fun onBackPressed() { - if (actionButton.isExpanded) { - actionButton.isExpanded = false - } else { + if (actionButton.isVisible) { super.onBackPressed() + } else { + toggleFab(false) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt index f41395bd..850c4b0d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt @@ -24,11 +24,16 @@ import androidx.preference.PreferenceManager import androidx.work.WorkManager import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory import com.keylesspalace.tusky.di.AppInjector -import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.EmojiCompatFont +import com.keylesspalace.tusky.util.LocaleManager +import com.keylesspalace.tusky.util.ThemeUtils import com.uber.autodispose.AutoDisposePlugins +import dagger.Lazy import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector import io.reactivex.plugins.RxJavaPlugins +import io.reactivex.schedulers.Schedulers import org.conscrypt.Conscrypt import java.security.Security import javax.inject.Inject @@ -37,11 +42,21 @@ class TuskyApplication : Application(), HasAndroidInjector { @Inject lateinit var androidInjector: DispatchingAndroidInjector + @Inject - lateinit var notificationWorkerFactory: NotificationWorkerFactory + lateinit var notificationWorkerFactory: Lazy override fun onCreate() { - + // Uncomment me to get StrictMode violation logs +// if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { +// StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder() +// .detectDiskReads() +// .detectDiskWrites() +// .detectNetwork() +// .detectUnbufferedIo() +// .penaltyLog() +// .build()) +// } super.onCreate() Security.insertProviderAt(Conscrypt.newProvider(), 1) @@ -53,7 +68,7 @@ class TuskyApplication : Application(), HasAndroidInjector { val preferences = PreferenceManager.getDefaultSharedPreferences(this) // init the custom emoji fonts - val emojiSelection = preferences.getInt(EmojiPreference.FONT_PREFERENCE, 0) + val emojiSelection = preferences.getInt(PrefKeys.EMOJI, 0) val emojiConfig = EmojiCompatFont.byId(emojiSelection) .getConfig(this) .setReplaceAll(true) @@ -63,16 +78,19 @@ class TuskyApplication : Application(), HasAndroidInjector { val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT) ThemeUtils.setAppNightMode(theme) - WorkManager.initialize( - this, - androidx.work.Configuration.Builder() - .setWorkerFactory(notificationWorkerFactory) - .build() - ) - RxJavaPlugins.setErrorHandler { Log.w("RxJava", "undeliverable exception", it) } + + // This will initialize the whole network stack and cache so we don't wan to wait for it + Schedulers.computation().scheduleDirect { + WorkManager.initialize( + this, + androidx.work.Configuration.Builder() + .setWorkerFactory(notificationWorkerFactory.get()) + .build() + ) + } } override fun attachBaseContext(base: Context) { diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt index 447c0590..5d90bd94 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt @@ -46,7 +46,7 @@ import com.bumptech.glide.request.FutureTarget import com.keylesspalace.tusky.BuildConfig.APPLICATION_ID import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.fragment.ViewImageFragment -import com.keylesspalace.tusky.pager.AvatarImagePagerAdapter +import com.keylesspalace.tusky.pager.SingleImagePagerAdapter import com.keylesspalace.tusky.pager.ImagePagerAdapter import com.keylesspalace.tusky.util.getTemporaryMediaFilename import com.keylesspalace.tusky.viewdata.AttachmentViewData @@ -68,7 +68,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener companion object { private const val EXTRA_ATTACHMENTS = "attachments" private const val EXTRA_ATTACHMENT_INDEX = "index" - private const val EXTRA_AVATAR_URL = "avatar" + private const val EXTRA_SINGLE_IMAGE_URL = "single_image" private const val TAG = "ViewMediaActivity" @JvmStatic @@ -79,9 +79,10 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener return intent } - fun newAvatarIntent(context: Context, url: String): Intent { + @JvmStatic + fun newSingleImageIntent(context: Context, url: String): Intent { val intent = Intent(context, ViewMediaActivity::class.java) - intent.putExtra(EXTRA_AVATAR_URL, url) + intent.putExtra(EXTRA_SINGLE_IMAGE_URL, url) return intent } } @@ -91,6 +92,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener private var attachments: ArrayList? = null private val toolbarVisibilityListeners = mutableListOf() + private var imageUrl: String? = null fun addToolbarVisibilityListener(listener: ToolbarVisibilityListener): Function0 { this.toolbarVisibilityListeners.add(listener) @@ -117,10 +119,10 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener ImagePagerAdapter(this, realAttachs, initialPosition) } else { - val avatarUrl = intent.getStringExtra(EXTRA_AVATAR_URL) - ?: throw IllegalArgumentException("attachment list or avatar url has to be set") + imageUrl = intent.getStringExtra(EXTRA_SINGLE_IMAGE_URL) + ?: throw IllegalArgumentException("attachment list or image url has to be set") - AvatarImagePagerAdapter(this, avatarUrl) + SingleImagePagerAdapter(this, imageUrl!!) } viewPager.adapter = adapter @@ -161,11 +163,10 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener } override fun onCreateOptionsMenu(menu: Menu): Boolean { - if (attachments != null) { - menuInflater.inflate(R.menu.view_media_toolbar, menu) - return true - } - return false + menuInflater.inflate(R.menu.view_media_toolbar, menu) + // We don't support 'open status' from single image views + menu?.findItem(R.id.action_open_status)?.isVisible = (attachments != null) + return true } override fun onPrepareOptionsMenu(menu: Menu?): Boolean { @@ -213,7 +214,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener } private fun downloadMedia() { - val url = attachments!![viewPager.currentItem].attachment.url + val url = imageUrl ?: attachments!![viewPager.currentItem].attachment.url val filename = Uri.parse(url).lastPathSegment Toast.makeText(applicationContext, resources.getString(R.string.download_image, filename), Toast.LENGTH_SHORT).show() @@ -240,8 +241,9 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener } private fun copyLink() { + val url = imageUrl ?: attachments!![viewPager.currentItem].attachment.url val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - clipboard.setPrimaryClip(ClipData.newPlainText(null, attachments!![viewPager.currentItem].attachment.url)) + clipboard.setPrimaryClip(ClipData.newPlainText(null, url)) } private fun shareMedia() { @@ -251,13 +253,17 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener return } - val attachment = attachments!![viewPager.currentItem].attachment - when (attachment.type) { - Attachment.Type.IMAGE -> shareImage(directory, attachment.url) - Attachment.Type.AUDIO, - Attachment.Type.VIDEO, - Attachment.Type.GIFV -> shareMediaFile(directory, attachment.url) - else -> Log.e(TAG, "Unknown media format for sharing.") + if (imageUrl != null) { + shareImage(directory, imageUrl!!) + } else { + val attachment = attachments!![viewPager.currentItem].attachment + when (attachment.type) { + Attachment.Type.IMAGE -> shareImage(directory, attachment.url) + Attachment.Type.AUDIO, + Attachment.Type.VIDEO, + Attachment.Type.GIFV -> shareMediaFile(directory, attachment.url) + else -> Log.e(TAG, "Unknown media format for sharing.") + } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt index ff87f978..dec4586b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt @@ -1,13 +1,19 @@ package com.keylesspalace.tusky.adapter +import android.graphics.Typeface +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.style.StyleSpan import android.view.View -import androidx.core.text.BidiFormatter import androidx.preference.PreferenceManager import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.R import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.interfaces.AccountActionListener -import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.loadAvatar +import com.keylesspalace.tusky.util.unicodeWrap +import com.keylesspalace.tusky.util.visible import kotlinx.android.synthetic.main.item_follow_request_notification.view.* internal class FollowRequestViewHolder(itemView: View, private val showHeader: Boolean) : RecyclerView.ViewHolder(itemView) { @@ -15,13 +21,16 @@ internal class FollowRequestViewHolder(itemView: View, private val showHeader: B private val animateAvatar: Boolean = PreferenceManager.getDefaultSharedPreferences(itemView.context) .getBoolean("animateGifAvatars", false) - fun setupWithAccount(account: Account, formatter: BidiFormatter?) { + fun setupWithAccount(account: Account) { id = account.id - val wrappedName = formatter?.unicodeWrap(account.name) ?: account.name + val wrappedName = account.name.unicodeWrap() val emojifiedName: CharSequence = wrappedName.emojify(account.emojis, itemView) itemView.displayNameTextView.text = emojifiedName if (showHeader) { - itemView.notificationTextView?.text = itemView.context.getString(R.string.notification_follow_request_format, emojifiedName) + val wholeMessage: String = itemView.context.getString(R.string.notification_follow_request_format, wrappedName) + itemView.notificationTextView?.text = SpannableStringBuilder(wholeMessage).apply { + setSpan(StyleSpan(Typeface.BOLD), 0, wrappedName.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + }.emojify(account.emojis, itemView) } itemView.notificationTextView?.visible(showHeader) val format = itemView.context.getString(R.string.status_username_format) diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java index 548d893c..dab3d4fe 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java @@ -53,7 +53,7 @@ public class FollowRequestsAdapter extends AccountAdapter { public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder; - holder.setupWithAccount(accountList.get(position), null); + holder.setupWithAccount(accountList.get(position)); holder.setupActionListener(accountActionListener); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java index 936d0edf..dcaf4169 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -35,7 +35,6 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; -import androidx.core.text.BidiFormatter; import androidx.recyclerview.widget.RecyclerView; import com.keylesspalace.tusky.R; @@ -51,6 +50,7 @@ import com.keylesspalace.tusky.util.ImageLoadingHelper; import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.util.SmartLengthInputFilter; import com.keylesspalace.tusky.util.StatusDisplayOptions; +import com.keylesspalace.tusky.util.StringUtils; import com.keylesspalace.tusky.util.TimestampUtils; import com.keylesspalace.tusky.viewdata.NotificationViewData; import com.keylesspalace.tusky.viewdata.StatusViewData; @@ -86,7 +86,6 @@ public class NotificationsAdapter extends RecyclerView.Adapter { private StatusActionListener statusListener; private NotificationActionListener notificationActionListener; private AccountActionListener accountActionListener; - private BidiFormatter bidiFormatter; private AdapterDataSource dataSource; public NotificationsAdapter(String accountId, @@ -102,7 +101,6 @@ public class NotificationsAdapter extends RecyclerView.Adapter { this.statusListener = statusListener; this.notificationActionListener = notificationActionListener; this.accountActionListener = accountActionListener; - bidiFormatter = BidiFormatter.getInstance(); } @NonNull @@ -204,7 +202,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { concreteNotificaton.getAccount().getAvatar()); } - holder.setMessage(concreteNotificaton, statusListener, bidiFormatter); + holder.setMessage(concreteNotificaton, statusListener); holder.setupButtons(notificationActionListener, concreteNotificaton.getAccount().getId(), concreteNotificaton.getId()); @@ -221,7 +219,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { case VIEW_TYPE_FOLLOW: { if (payloadForHolder == null) { FollowViewHolder holder = (FollowViewHolder) viewHolder; - holder.setMessage(concreteNotificaton.getAccount(), bidiFormatter); + holder.setMessage(concreteNotificaton.getAccount()); holder.setupButtons(notificationActionListener, concreteNotificaton.getAccount().getId()); } break; @@ -229,7 +227,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { case VIEW_TYPE_FOLLOW_REQUEST: { if (payloadForHolder == null) { FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder; - holder.setupWithAccount(concreteNotificaton.getAccount(), bidiFormatter); + holder.setupWithAccount(concreteNotificaton.getAccount()); holder.setupActionListener(accountActionListener); } } @@ -325,11 +323,11 @@ public class NotificationsAdapter extends RecyclerView.Adapter { this.statusDisplayOptions = statusDisplayOptions; } - void setMessage(Account account, BidiFormatter bidiFormatter) { + void setMessage(Account account) { Context context = message.getContext(); String format = context.getString(R.string.notification_follow_format); - String wrappedDisplayName = bidiFormatter.unicodeWrap(account.getName()); + String wrappedDisplayName = StringUtils.unicodeWrap(account.getName()); String wholeMessage = String.format(format, wrappedDisplayName); CharSequence emojifiedMessage = CustomEmojiHelper.emojify(wholeMessage, account.getEmojis(), message); message.setText(emojifiedMessage); @@ -459,10 +457,10 @@ public class NotificationsAdapter extends RecyclerView.Adapter { } } - void setMessage(NotificationViewData.Concrete notificationViewData, LinkListener listener, BidiFormatter bidiFormatter) { + void setMessage(NotificationViewData.Concrete notificationViewData, LinkListener listener) { this.statusViewData = notificationViewData.getStatusViewData(); - String displayName = bidiFormatter.unicodeWrap(notificationViewData.getAccount().getName()); + String displayName = StringUtils.unicodeWrap(notificationViewData.getAccount().getName()); Notification.Type type = notificationViewData.getType(); Context context = message.getContext(); diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt index 8da82d22..a990d326 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt @@ -37,18 +37,21 @@ class PollAdapter: RecyclerView.Adapter() { private var votersCount: Int? = null private var mode = RESULT private var emojis: List = emptyList() + private var resultClickListener: View.OnClickListener? = null fun setup( options: List, voteCount: Int, votersCount: Int?, emojis: List, - mode: Int) { + mode: Int, + resultClickListener: View.OnClickListener?) { this.pollOptions = options this.voteCount = voteCount this.votersCount = votersCount this.emojis = emojis this.mode = mode + this.resultClickListener = resultClickListener notifyDataSetChanged() } @@ -84,7 +87,7 @@ class PollAdapter: RecyclerView.Adapter() { val level = percent * 100 holder.resultTextView.background.level = level - + holder.resultTextView.setOnClickListener(resultClickListener) } SINGLE -> { val emojifiedPollOptionText = option.title.emojify(emojis, holder.radioButton) diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java index c159138b..3869ca02 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -26,10 +26,12 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; +import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.load.resource.bitmap.CenterCrop; import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners; import com.google.android.material.button.MaterialButton; import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.ViewMediaActivity; import com.keylesspalace.tusky.entity.Attachment; import com.keylesspalace.tusky.entity.Attachment.Focus; import com.keylesspalace.tusky.entity.Attachment.MetaData; @@ -615,7 +617,13 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { final String accountId, final String statusContent, StatusDisplayOptions statusDisplayOptions) { - avatar.setOnClickListener(v -> listener.onViewAccount(accountId)); + View.OnClickListener profileButtonClickListener = button -> { + listener.onViewAccount(accountId); + }; + + avatar.setOnClickListener(profileButtonClickListener); + displayName.setOnClickListener(profileButtonClickListener); + replyButton.setOnClickListener(v -> { int position = getAdapterPosition(); if (position != RecyclerView.NO_POSITION) { @@ -734,7 +742,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { } if (cardView != null) { - setupCard(status, statusDisplayOptions.cardViewMode()); + setupCard(status, statusDisplayOptions.cardViewMode(), statusDisplayOptions); } setupButtons(listener, status.getSenderId(), status.getContent().toString(), @@ -915,12 +923,18 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { if (expired || poll.getVoted()) { // no voting possible - pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), poll.getVotersCount(), emojis, PollAdapter.RESULT); + View.OnClickListener viewThreadListener = v -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onViewThread(position); + } + }; + pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), poll.getVotersCount(), emojis, PollAdapter.RESULT, viewThreadListener); pollButton.setVisibility(View.GONE); } else { // voting possible - pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), poll.getVotersCount(), emojis, poll.getMultiple() ? PollAdapter.MULTIPLE : PollAdapter.SINGLE); + pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), poll.getVotersCount(), emojis, poll.getMultiple() ? PollAdapter.MULTIPLE : PollAdapter.SINGLE, null); pollButton.setVisibility(View.VISIBLE); @@ -964,15 +978,14 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { if (statusDisplayOptions.useAbsoluteTime()) { pollDurationInfo = context.getString(R.string.poll_info_time_absolute, getAbsoluteTime(poll.getExpiresAt())); } else { - String pollDuration = TimestampUtils.formatPollDuration(pollDescription.getContext(), poll.getExpiresAt().getTime(), timestamp); - pollDurationInfo = context.getString(R.string.poll_info_time_relative, pollDuration); + pollDurationInfo = TimestampUtils.formatPollDuration(pollDescription.getContext(), poll.getExpiresAt().getTime(), timestamp); } } return pollDescription.getContext().getString(R.string.poll_info_format, votesText, pollDurationInfo); } - protected void setupCard(StatusViewData.Concrete status, CardViewMode cardViewMode) { + protected void setupCard(StatusViewData.Concrete status, CardViewMode cardViewMode, StatusDisplayOptions statusDisplayOptions) { if (cardViewMode != CardViewMode.NONE && status.getAttachments().size() == 0 && status.getCard() != null && @@ -994,7 +1007,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { cardUrl.setText(card.getUrl()); - if (!TextUtils.isEmpty(card.getImage())) { + // Statuses from other activitypub sources can be marked sensitive even if there's no media, + // so let's blur the preview in that case + // If media previews are disabled, show placeholder for cards as well + if (statusDisplayOptions.mediaPreviewEnabled() && !status.isSensitive() && !TextUtils.isEmpty(card.getImage())) { int topLeftRadius = 0; int topRightRadius = 0; @@ -1025,12 +1041,29 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { bottomLeftRadius = radius; } + RequestBuilder builder = Glide.with(cardImage).load(card.getImage()); + if (statusDisplayOptions.useBlurhash() && !TextUtils.isEmpty(card.getBlurhash())) { + builder = builder.placeholder(decodeBlurHash(card.getBlurhash())); + } + builder.transform( + new CenterCrop(), + new GranularRoundedCorners(topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius) + ) + .into(cardImage); + } else if (statusDisplayOptions.useBlurhash() && !TextUtils.isEmpty(card.getBlurhash())) { + int radius = cardImage.getContext().getResources() + .getDimensionPixelSize(R.dimen.card_radius); - Glide.with(cardImage) - .load(card.getImage()) + cardView.setOrientation(LinearLayout.HORIZONTAL); + cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT; + cardImage.getLayoutParams().width = cardImage.getContext().getResources() + .getDimensionPixelSize(R.dimen.card_image_horizontal_width); + cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT; + cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT; + Glide.with(cardImage).load(decodeBlurHash(card.getBlurhash())) .transform( new CenterCrop(), - new GranularRoundedCorners(topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius) + new GranularRoundedCorners(radius, 0, 0, radius) ) .into(cardImage); } else { @@ -1043,7 +1076,15 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { cardImage.setImageResource(R.drawable.card_image_placeholder); } - cardView.setOnClickListener(v -> LinkHelper.openLink(card.getUrl(), v.getContext())); + View.OnClickListener visitLink = v -> LinkHelper.openLink(card.getUrl(), v.getContext()); + View.OnClickListener openImage = v -> cardView.getContext().startActivity(ViewMediaActivity.newSingleImageIntent(cardView.getContext(), card.getEmbed_url())); + + cardInfo.setOnClickListener(visitLink); + // View embedded photos in our image viewer instead of opening the browser + cardImage.setOnClickListener(card.getType().equals(Card.TYPE_PHOTO) && !TextUtils.isEmpty(card.getEmbed_url()) ? + openImage : + visitLink); + cardView.setClipToOutline(true); } else { cardView.setVisibility(View.GONE); diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java index 0d844b91..6350dde0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java @@ -106,7 +106,7 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder { StatusDisplayOptions statusDisplayOptions, @Nullable Object payloads) { super.setupWithStatus(status, listener, statusDisplayOptions, payloads); - setupCard(status, CardViewMode.FULL_WIDTH); // Always show card for detailed status + setupCard(status, CardViewMode.FULL_WIDTH, statusDisplayOptions); // Always show card for detailed status if (payloads == null) { setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener); diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt index 007ba546..cd877a15 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt @@ -36,8 +36,8 @@ interface ItemInteractionListener { fun onTabRemoved(position: Int) fun onStartDelete(viewHolder: RecyclerView.ViewHolder) fun onStartDrag(viewHolder: RecyclerView.ViewHolder) - fun onActionChipClicked(tab: TabData) - fun onChipClicked(tab: TabData, chipPosition: Int) + fun onActionChipClicked(tab: TabData, tabPosition: Int) + fun onChipClicked(tab: TabData, tabPosition: Int, chipPosition: Int) } class TabAdapter(private var data: List, @@ -62,16 +62,17 @@ class TabAdapter(private var data: List, override fun onBindViewHolder(holder: ViewHolder, position: Int) { val context = holder.itemView.context - if (!small && data[position].id == LIST) { - holder.itemView.textView.text = data[position].arguments.getOrNull(1).orEmpty() + val tab = data[position] + if (!small && tab.id == LIST) { + holder.itemView.textView.text = tab.arguments.getOrNull(1).orEmpty() } else { - holder.itemView.textView.setText(data[position].text) + holder.itemView.textView.setText(tab.text) } - val iconDrawable = ThemeUtils.getTintedDrawable(context, data[position].icon, android.R.attr.textColorSecondary) + val iconDrawable = ThemeUtils.getTintedDrawable(context, tab.icon, android.R.attr.textColorSecondary) holder.itemView.textView.setCompoundDrawablesRelativeWithIntrinsicBounds(iconDrawable, null, null, null) if (small) { holder.itemView.textView.setOnClickListener { - listener.onTabAdded(data[position]) + listener.onTabAdded(tab) } } holder.itemView.imageView?.setOnTouchListener { _, event -> @@ -96,7 +97,7 @@ class TabAdapter(private var data: List, if (!small) { - if (data[position].id == HASHTAG) { + if (tab.id == HASHTAG) { holder.itemView.chipGroup.show() /* @@ -104,34 +105,33 @@ class TabAdapter(private var data: List, * The other dynamic chips are inserted in front of the actionChip. * This code tries to reuse already added chips to reduce the number of Views created. */ - data[position].arguments.forEachIndexed { i, arg -> + tab.arguments.forEachIndexed { i, arg -> val chip = holder.itemView.chipGroup.getChildAt(i).takeUnless { it.id == R.id.actionChip } as Chip? ?: Chip(context).apply { - text = arg holder.itemView.chipGroup.addView(this, holder.itemView.chipGroup.size - 1) } chip.text = arg - if(data[position].arguments.size <= 1) { + if(tab.arguments.size <= 1) { chip.chipIcon = null chip.setOnClickListener(null) } else { val cancelIcon = ThemeUtils.getTintedDrawable(context, R.drawable.ic_cancel_24dp, android.R.attr.textColorPrimary) chip.chipIcon = cancelIcon chip.setOnClickListener { - listener.onChipClicked(data[position], i) + listener.onChipClicked(tab, holder.adapterPosition, i) } } } - while(holder.itemView.chipGroup.size - 1 > data[position].arguments.size) { - holder.itemView.chipGroup.removeViewAt(data[position].arguments.size - 1) + while(holder.itemView.chipGroup.size - 1 > tab.arguments.size) { + holder.itemView.chipGroup.removeViewAt(tab.arguments.size) } holder.itemView.actionChip.setOnClickListener { - listener.onActionChipClicked(data[position]) + listener.onActionChipClicked(tab, holder.adapterPosition) } } else { @@ -140,9 +140,7 @@ class TabAdapter(private var data: List, } } - override fun getItemCount(): Int { - return data.size - } + override fun getItemCount() = data.size fun setRemoveButtonVisible(enabled: Boolean) { if (removeButtonEnabled != enabled) { diff --git a/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt b/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt index 8e62faeb..288de430 100644 --- a/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt +++ b/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt @@ -19,4 +19,5 @@ data class ProfileEditedEvent(val newProfileData: Account) : Dispatchable data class PreferenceChangedEvent(val preferenceKey: String) : Dispatchable data class MainTabsChangedEvent(val newTabs: List) : Dispatchable data class PollVoteEvent(val statusId: String, val poll: Poll) : Dispatchable -data class DomainMuteEvent(val instance: String): Dispatchable \ No newline at end of file +data class DomainMuteEvent(val instance: String): Dispatchable +data class AnnouncementReadEvent(val announcementId: String): Dispatchable diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt new file mode 100644 index 00000000..c4fa93f2 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt @@ -0,0 +1,115 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.announcements + +import android.view.ContextThemeWrapper +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.view.size +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.chip.Chip +import com.google.android.material.chip.ChipGroup +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.entity.Announcement +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.util.emojify +import kotlinx.android.synthetic.main.item_announcement.view.* + +interface AnnouncementActionListener { + fun openReactionPicker(announcementId: String, target: View) + fun addReaction(announcementId: String, name: String) + fun removeReaction(announcementId: String, name: String) +} + +class AnnouncementAdapter( + private var items: List = emptyList(), + private val listener: AnnouncementActionListener +) : RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnnouncementViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_announcement, parent, false) + return AnnouncementViewHolder(view) + } + + override fun onBindViewHolder(viewHolder: AnnouncementViewHolder, position: Int) { + viewHolder.bind(items[position]) + } + + override fun getItemCount() = items.size + + fun updateList(items: List) { + this.items = items + notifyDataSetChanged() + } + + inner class AnnouncementViewHolder(private val view: View) : RecyclerView.ViewHolder(view) { + + private val text: TextView = view.text + private val chips: ChipGroup = view.chipGroup + private val addReactionChip: Chip = view.addReactionChip + + fun bind(item: Announcement) { + text.text = item.content + + item.reactions.forEachIndexed { i, reaction -> + (chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip? + ?: Chip(ContextThemeWrapper(view.context, R.style.Widget_MaterialComponents_Chip_Choice)).apply { + isCheckable = true + checkedIcon = null + chips.addView(this, i) + }) + .apply { + val emojiText = if (reaction.url == null) { + reaction.name + } else { + view.context.getString(R.string.emoji_shortcode_format, reaction.name) + } + text = ("$emojiText ${reaction.count}") + .emojify( + listOf(Emoji( + reaction.name, + reaction.url ?: "", + reaction.staticUrl ?: "", + null + )), + this + ) + + isChecked = reaction.me + + setOnClickListener { + if (reaction.me) { + listener.removeReaction(item.id, reaction.name) + } else { + listener.addReaction(item.id, reaction.name) + } + } + } + } + + while (chips.size - 1 > item.reactions.size) { + chips.removeViewAt(item.reactions.size) + } + + addReactionChip.setOnClickListener { + listener.openReactionPicker(item.id, it) + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt new file mode 100644 index 00000000..5bfd17bc --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt @@ -0,0 +1,153 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.announcements + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.widget.PopupWindow +import androidx.activity.viewModels +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.adapter.EmojiAdapter +import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener +import com.keylesspalace.tusky.di.Injectable +import com.keylesspalace.tusky.di.ViewModelFactory +import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.view.EmojiPicker +import kotlinx.android.synthetic.main.activity_announcements.* +import kotlinx.android.synthetic.main.toolbar_basic.* +import javax.inject.Inject + +class AnnouncementsActivity : BaseActivity(), AnnouncementActionListener, OnEmojiSelectedListener, Injectable { + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + private val viewModel: AnnouncementsViewModel by viewModels { viewModelFactory } + + private val adapter = AnnouncementAdapter(emptyList(), this) + + private val picker by lazy { EmojiPicker(this) } + private val pickerDialog by lazy { + PopupWindow(this) + .apply { + contentView = picker + isFocusable = true + setOnDismissListener { + currentAnnouncementId = null + } + } + } + private var currentAnnouncementId: String? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_announcements) + + setSupportActionBar(toolbar) + supportActionBar?.apply { + title = getString(R.string.title_announcements) + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + swipeRefreshLayout.setOnRefreshListener(this::refreshAnnouncements) + swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) + + announcementsList.setHasFixedSize(true) + announcementsList.layoutManager = LinearLayoutManager(this) + val divider = DividerItemDecoration(this, DividerItemDecoration.VERTICAL) + announcementsList.addItemDecoration(divider) + announcementsList.adapter = adapter + + viewModel.announcements.observe(this) { + when (it) { + is Success -> { + progressBar.hide() + swipeRefreshLayout.isRefreshing = false + if (it.data.isNullOrEmpty()) { + errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_announcements) + errorMessageView.show() + } else { + errorMessageView.hide() + } + adapter.updateList(it.data ?: listOf()) + } + is Loading -> { + errorMessageView.hide() + } + is Error -> { + progressBar.hide() + swipeRefreshLayout.isRefreshing = false + errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) { + refreshAnnouncements() + } + errorMessageView.show() + } + } + } + + viewModel.emojis.observe(this) { + picker.adapter = EmojiAdapter(it, this) + } + + viewModel.load() + progressBar.show() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + onBackPressed() + return true + } + } + return super.onOptionsItemSelected(item) + } + + private fun refreshAnnouncements() { + viewModel.load() + swipeRefreshLayout.isRefreshing = true + } + + override fun openReactionPicker(announcementId: String, target: View) { + currentAnnouncementId = announcementId + pickerDialog.showAsDropDown(target) + } + + override fun onEmojiSelected(shortcode: String) { + viewModel.addReaction(currentAnnouncementId!!, shortcode) + pickerDialog.dismiss() + } + + override fun addReaction(announcementId: String, name: String) { + viewModel.addReaction(announcementId, name) + } + + override fun removeReaction(announcementId: String, name: String) { + viewModel.removeReaction(announcementId, name) + } + + companion object { + fun newIntent(context: Context) = Intent(context, AnnouncementsActivity::class.java) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt new file mode 100644 index 00000000..964cc739 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt @@ -0,0 +1,187 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.announcements + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.keylesspalace.tusky.appstore.AnnouncementReadEvent +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.InstanceEntity +import com.keylesspalace.tusky.entity.Announcement +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.entity.Instance +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.* +import io.reactivex.rxkotlin.Singles +import javax.inject.Inject + +class AnnouncementsViewModel @Inject constructor( + accountManager: AccountManager, + private val appDatabase: AppDatabase, + private val mastodonApi: MastodonApi, + private val eventHub: EventHub +) : RxAwareViewModel() { + + private val announcementsMutable = MutableLiveData>>() + val announcements: LiveData>> = announcementsMutable + + private val emojisMutable = MutableLiveData>() + val emojis: LiveData> = emojisMutable + + init { + Singles.zip( + mastodonApi.getCustomEmojis(), + appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!) + .map> { Either.Left(it) } + .onErrorResumeNext( + mastodonApi.getInstance() + .map { Either.Right(it) } + ) + ) { emojis, either -> + either.asLeftOrNull()?.copy(emojiList = emojis) + ?: InstanceEntity( + accountManager.activeAccount?.domain!!, + emojis, + either.asRight().maxTootChars, + either.asRight().pollLimits?.maxOptions, + either.asRight().pollLimits?.maxOptionChars, + either.asRight().version + ) + } + .doOnSuccess { + appDatabase.instanceDao().insertOrReplace(it) + } + .subscribe({ + emojisMutable.postValue(it.emojiList) + }, { + Log.w(TAG, "Failed to get custom emojis.", it) + }) + .autoDispose() + } + + fun load() { + announcementsMutable.postValue(Loading()) + mastodonApi.listAnnouncements() + .subscribe({ + announcementsMutable.postValue(Success(it)) + it.filter { announcement -> !announcement.read } + .forEach { announcement -> + mastodonApi.dismissAnnouncement(announcement.id) + .subscribe( + { + eventHub.dispatch(AnnouncementReadEvent(announcement.id)) + }, + { throwable -> + Log.d(TAG, "Failed to mark announcement as read.", throwable) + } + ) + .autoDispose() + } + }, { + announcementsMutable.postValue(Error(cause = it)) + }) + .autoDispose() + } + + fun addReaction(announcementId: String, name: String) { + mastodonApi.addAnnouncementReaction(announcementId, name) + .subscribe({ + announcementsMutable.postValue( + Success( + announcements.value!!.data!!.map { announcement -> + if (announcement.id == announcementId) { + announcement.copy( + reactions = if (announcement.reactions.find { reaction -> reaction.name == name } != null) { + announcement.reactions.map { reaction -> + if (reaction.name == name) { + reaction.copy( + count = reaction.count + 1, + me = true + ) + } else { + reaction + } + } + } else { + listOf( + *announcement.reactions.toTypedArray(), + emojis.value!!.find { emoji -> emoji.shortcode == name } + !!.run { + Announcement.Reaction( + name, + 1, + true, + url, + staticUrl + ) + } + ) + } + ) + } else { + announcement + } + } + ) + ) + }, { + Log.w(TAG, "Failed to add reaction to the announcement.", it) + }) + .autoDispose() + } + + fun removeReaction(announcementId: String, name: String) { + mastodonApi.removeAnnouncementReaction(announcementId, name) + .subscribe({ + announcementsMutable.postValue( + Success( + announcements.value!!.data!!.map { announcement -> + if (announcement.id == announcementId) { + announcement.copy( + reactions = announcement.reactions.mapNotNull { reaction -> + if (reaction.name == name) { + if (reaction.count > 1) { + reaction.copy( + count = reaction.count - 1, + me = false + ) + } else { + null + } + } else { + reaction + } + } + ) + } else { + announcement + } + } + ) + ) + }, { + Log.w(TAG, "Failed to remove reaction from the announcement.", it) + }) + .autoDispose() + } + + companion object { + private const val TAG = "AnnouncementsViewModel" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt index 6b0f5745..4eba7e32 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt @@ -49,9 +49,7 @@ import androidx.core.view.inputmethod.InputConnectionCompat import androidx.core.view.inputmethod.InputContentInfoCompat import androidx.core.view.isGone import androidx.core.view.isVisible -import androidx.lifecycle.Observer import androidx.preference.PreferenceManager -import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.transition.TransitionManager import com.google.android.material.bottomsheet.BottomSheetBehavior @@ -150,7 +148,7 @@ class ComposeActivity : BaseActivity(), /* 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) { - this.composeOptions = intent.getParcelableExtra(COMPOSE_OPTIONS_EXTRA) + this.composeOptions = intent.getParcelableExtra(COMPOSE_OPTIONS_EXTRA) viewModel.setup(composeOptions) setupReplyViews(composeOptions?.replyingStatusAuthor) val tootText = composeOptions?.tootText @@ -302,7 +300,7 @@ class ComposeActivity : BaseActivity(), } viewModel.media.observe { media -> mediaAdapter.submitList(media) - if(media.size != mediaCount) { + if (media.size != mediaCount) { mediaCount = media.size composeMediaPreviewBar.visible(media.isNotEmpty()) updateSensitiveMediaToggle(viewModel.markMediaAsSensitive.value != false, viewModel.showContentWarning.value != false) @@ -312,8 +310,8 @@ class ComposeActivity : BaseActivity(), pollPreview.visible(poll != null) poll?.let(pollPreview::setPoll) } - viewModel.scheduledAt.observe {scheduledAt -> - if(scheduledAt == null) { + viewModel.scheduledAt.observe { scheduledAt -> + if (scheduledAt == null) { composeScheduleView.resetSchedule() } else { composeScheduleView.setDateTime(scheduledAt) @@ -345,7 +343,6 @@ class ComposeActivity : BaseActivity(), scheduleBehavior = BottomSheetBehavior.from(composeScheduleView) emojiBehavior = BottomSheetBehavior.from(emojiView) - emojiView.layoutManager = GridLayoutManager(this, 3, GridLayoutManager.HORIZONTAL, false) enableButton(composeEmojiButton, clickable = false, colorActive = false) // Setup the interface buttons. @@ -553,7 +550,7 @@ class ComposeActivity : BaseActivity(), } private fun onScheduleClick() { - if(viewModel.scheduledAt.value == null) { + if (viewModel.scheduledAt.value == null) { composeScheduleView.openPickDateDialog() } else { showScheduleView() @@ -682,7 +679,15 @@ class ComposeActivity : BaseActivity(), } private fun updateVisibleCharactersLeft() { - composeCharactersLeftView.text = String.format(Locale.getDefault(), "%d", maximumTootCharacters - calculateTextLength()) + val remainingLength = maximumTootCharacters - calculateTextLength() + composeCharactersLeftView.text = String.format(Locale.getDefault(), "%d", remainingLength) + + val textColor = if (remainingLength < 0) { + ContextCompat.getColor(this, R.color.tusky_red) + } else { + ThemeUtils.getColor(this, android.R.attr.textColorTertiary) + } + composeCharactersLeftView.setTextColor(textColor) } private fun onContentWarningChanged() { @@ -708,9 +713,9 @@ class ComposeActivity : BaseActivity(), // Verify the returned content's type is of the correct MIME type val supported = inputContentInfo.description.hasMimeType("image/*") - if(supported) { + if (supported) { val lacksPermission = (flags and InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0 - if(lacksPermission) { + if (lacksPermission) { try { inputContentInfo.requestPermission() } catch (e: Exception) { @@ -737,11 +742,13 @@ class ComposeActivity : BaseActivity(), composeEditField.error = getString(R.string.error_empty) enableButtons(true) } else if (characterCount <= maximumTootCharacters) { - finishingUploadDialog = ProgressDialog.show( - this, getString(R.string.dialog_title_finishing_media_upload), - getString(R.string.dialog_message_uploading_media), true, true) + if (viewModel.media.value!!.isNotEmpty()) { + finishingUploadDialog = ProgressDialog.show( + this, getString(R.string.dialog_title_finishing_media_upload), + getString(R.string.dialog_message_uploading_media), true, true) + } - viewModel.sendStatus(contentText, spoilerText).observe(this, Observer { + viewModel.sendStatus(contentText, spoilerText).observe(this, { finishingUploadDialog?.dismiss() deleteDraftAndFinish() }) @@ -762,7 +769,7 @@ class ComposeActivity : BaseActivity(), Snackbar.LENGTH_SHORT).apply { } - bar.setAction(R.string.action_retry) { onMediaPick()} + bar.setAction(R.string.action_retry) { onMediaPick() } //necessary so snackbar is shown over everything bar.view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation) bar.show() @@ -904,7 +911,7 @@ class ComposeActivity : BaseActivity(), override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { Log.d(TAG, event.toString()) - if(event.action == KeyEvent.ACTION_DOWN) { + if (event.action == KeyEvent.ACTION_DOWN) { if (event.isCtrlPressed) { if (keyCode == KeyEvent.KEYCODE_ENTER) { // send toot by pressing CTRL + ENTER @@ -994,6 +1001,7 @@ class ComposeActivity : BaseActivity(), @Parcelize data class ComposeOptions( // Let's keep fields var until all consumers are Kotlin + var scheduledTootUid: String? = null, var savedTootUid: Int? = null, var tootText: String? = null, var mediaUrls: List? = null, @@ -1008,7 +1016,8 @@ class ComposeActivity : BaseActivity(), var mediaAttachments: List? = null, var scheduledAt: String? = null, var sensitive: Boolean? = null, - var poll: NewPoll? = null + var poll: NewPoll? = null, + var modifiedInitialState: Boolean? = null ) : Parcelable companion object { @@ -1017,7 +1026,7 @@ class ComposeActivity : BaseActivity(), private const val MEDIA_TAKE_PHOTO_RESULT = 2 private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1 - private const val COMPOSE_OPTIONS_EXTRA = "COMPOSE_OPTIONS" + internal 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 diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt index d38fe94b..2c015899 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt @@ -33,6 +33,7 @@ import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.service.ServiceClient import com.keylesspalace.tusky.service.TootToSend import com.keylesspalace.tusky.util.* +import io.reactivex.Observable.just import io.reactivex.disposables.Disposable import io.reactivex.rxkotlin.Singles import java.util.* @@ -58,11 +59,13 @@ class ComposeViewModel private var replyingStatusContent: String? = null internal var startingText: String? = null private var savedTootUid: Int = 0 + private var scheduledTootUid: String? = null private var startingContentWarning: String = "" private var inReplyToId: String? = null private var startingVisibility: Status.Visibility = Status.Visibility.UNKNOWN private var contentWarningStateChanged: Boolean = false + private var modifiedInitialState: Boolean = false private val instance: MutableLiveData = MutableLiveData(null) @@ -93,6 +96,7 @@ class ComposeViewModel private val mediaToDisposable = mutableMapOf() + private val isEditingScheduledToot get() = !scheduledTootUid.isNullOrEmpty() init { @@ -197,7 +201,7 @@ class ComposeViewModel val mediaChanged = !media.value.isNullOrEmpty() val pollChanged = poll.value != null - return textChanged || contentWarningChanged || mediaChanged || pollChanged + return modifiedInitialState || textChanged || contentWarningChanged || mediaChanged || pollChanged } fun contentWarningChanged(value: Boolean) { @@ -240,7 +244,14 @@ class ComposeViewModel content: String, spoilerText: String ): LiveData { - return media + + val deletionObservable = if (isEditingScheduledToot) { + api.deleteScheduledStatus(scheduledTootUid.toString()).toObservable().map { Unit } + } else { + just(Unit) + }.toLiveData() + + val sendObservable = media .filter { items -> items.all { it.uploadPercent == -1 } } .map { val mediaIds = ArrayList() @@ -271,8 +282,13 @@ class ComposeViewModel idempotencyKey = randomAlphanumericString(16), retries = 0 ) + serviceClient.sendToot(tootToSend) } + + return combineLiveData(deletionObservable, sendObservable) { _, _ -> Unit } + + } fun updateDescription(localId: Long, description: String): LiveData { @@ -369,7 +385,7 @@ class ComposeViewModel preferredVisibility.num.coerceAtLeast(replyVisibility.num)) inReplyToId = composeOptions?.inReplyToId - + modifiedInitialState = composeOptions?.modifiedInitialState == true val contentWarning = composeOptions?.contentWarning if (contentWarning != null) { @@ -405,6 +421,7 @@ class ComposeViewModel savedTootUid = composeOptions?.savedTootUid ?: 0 + scheduledTootUid = composeOptions?.scheduledTootUid startingText = composeOptions?.tootText diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt index dc2d60c8..b1ab32d7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt @@ -21,8 +21,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.viewModels -import androidx.lifecycle.Observer -import androidx.paging.PagedList import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager @@ -36,7 +34,10 @@ import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.fragment.SFragment import com.keylesspalace.tusky.interfaces.ReselectableFragment import com.keylesspalace.tusky.interfaces.StatusActionListener -import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.util.CardViewMode +import com.keylesspalace.tusky.util.NetworkState +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.util.hide import kotlinx.android.synthetic.main.fragment_timeline.* import javax.inject.Inject @@ -83,21 +84,21 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res initSwipeToRefresh() - viewModel.conversations.observe(viewLifecycleOwner, Observer> { + viewModel.conversations.observe(viewLifecycleOwner) { adapter.submitList(it) - }) - viewModel.networkState.observe(viewLifecycleOwner, Observer { + } + viewModel.networkState.observe(viewLifecycleOwner) { adapter.setNetworkState(it) - }) + } viewModel.load() } private fun initSwipeToRefresh() { - viewModel.refreshState.observe(viewLifecycleOwner, Observer { + viewModel.refreshState.observe(viewLifecycleOwner) { swipeRefreshLayout.isRefreshing = it == NetworkState.LOADING - }) + } swipeRefreshLayout.setOnRefreshListener { viewModel.refresh() } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationFetcher.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationFetcher.kt new file mode 100644 index 00000000..394a6846 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationFetcher.kt @@ -0,0 +1,82 @@ +package com.keylesspalace.tusky.components.notifications + +import android.util.Log +import com.keylesspalace.tusky.db.AccountEntity +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.entity.Marker +import com.keylesspalace.tusky.entity.Notification +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.isLessThan +import javax.inject.Inject + +class NotificationFetcher @Inject constructor( + private val mastodonApi: MastodonApi, + private val accountManager: AccountManager, + private val notifier: Notifier +) { + fun fetchAndShow() { + for (account in accountManager.getAllAccountsOrderedByActive()) { + if (account.notificationsEnabled) { + try { + val notifications = fetchNotifications(account) + notifications.forEachIndexed { index, notification -> + notifier.show(notification, account, index == 0) + } + accountManager.saveAccount(account) + } catch (e: Exception) { + Log.w(TAG, "Error while fetching notifications", e) + } + } + } + } + + private fun fetchNotifications(account: AccountEntity): MutableList { + val authHeader = String.format("Bearer %s", account.accessToken) + // We fetch marker to not load/show notifications which user has already seen + val marker = fetchMarker(authHeader, account) + if (marker != null && account.lastNotificationId.isLessThan(marker.lastReadId)) { + account.lastNotificationId = marker.lastReadId + } + Log.d(TAG, "getting Notifications for " + account.fullName) + val notifications = mastodonApi.notificationsWithAuth( + authHeader, + account.domain, + account.lastNotificationId + ).blockingGet() + + val newId = account.lastNotificationId + var newestId = "" + val result = mutableListOf() + for (notification in notifications.reversed()) { + val currentId = notification.id + if (newestId.isLessThan(currentId)) { + newestId = currentId + account.lastNotificationId = currentId + } + if (newId.isLessThan(currentId)) { + result.add(notification) + } + } + return result + } + + private fun fetchMarker(authHeader: String, account: AccountEntity): Marker? { + return try { + val allMarkers = mastodonApi.markersWithAuth( + authHeader, + account.domain, + listOf("notifications") + ).blockingGet() + val notificationMarker = allMarkers["notifications"] + Log.d(TAG, "Fetched marker: $notificationMarker") + notificationMarker + } catch (e: Exception) { + Log.e(TAG, "Failed to fetch marker", e) + null + } + } + + companion object { + const val TAG = "NotificationFetcher" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java index a094750e..3020f207 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java @@ -37,7 +37,6 @@ import androidx.core.app.NotificationManagerCompat; import androidx.core.app.RemoteInput; import androidx.core.app.TaskStackBuilder; import androidx.core.content.ContextCompat; -import androidx.core.text.BidiFormatter; import androidx.work.Constraints; import androidx.work.NetworkType; import androidx.work.PeriodicWorkRequest; @@ -58,6 +57,7 @@ import com.keylesspalace.tusky.entity.PollOption; import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.receiver.NotificationClearBroadcastReceiver; import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver; +import com.keylesspalace.tusky.util.StringUtils; import com.keylesspalace.tusky.viewdata.PollViewDataKt; import org.json.JSONArray; @@ -145,7 +145,6 @@ public class NotificationHelper { String rawCurrentNotifications = account.getActiveNotifications(); JSONArray currentNotifications; - BidiFormatter bidiFormatter = BidiFormatter.getInstance(); try { currentNotifications = new JSONArray(rawCurrentNotifications); @@ -174,7 +173,7 @@ public class NotificationHelper { notificationId++; - builder.setContentTitle(titleForType(context, body, bidiFormatter, account)) + builder.setContentTitle(titleForType(context, body, account)) .setContentText(bodyForType(body, context)); if (body.getType() == Notification.Type.MENTION || body.getType() == Notification.Type.POLL) { @@ -243,7 +242,7 @@ public class NotificationHelper { if (currentNotifications.length() != 1) { try { String title = context.getString(R.string.notification_title_summary, currentNotifications.length()); - String text = joinNames(context, currentNotifications, bidiFormatter); + String text = joinNames(context, currentNotifications); summaryBuilder.setContentTitle(title) .setContentText(text); } catch (JSONException e) { @@ -573,36 +572,36 @@ public class NotificationHelper { } } - private static String wrapItemAt(JSONArray array, int index, BidiFormatter bidiFormatter) throws JSONException { - return bidiFormatter.unicodeWrap(array.get(index).toString()); + private static String wrapItemAt(JSONArray array, int index) throws JSONException { + return StringUtils.unicodeWrap(array.get(index).toString()); } @Nullable - private static String joinNames(Context context, JSONArray array, BidiFormatter bidiFormatter) throws JSONException { + private static String joinNames(Context context, JSONArray array) throws JSONException { if (array.length() > 3) { int length = array.length(); return String.format(context.getString(R.string.notification_summary_large), - wrapItemAt(array, length - 1, bidiFormatter), - wrapItemAt(array, length - 2, bidiFormatter), - wrapItemAt(array, length - 3, bidiFormatter), + wrapItemAt(array, length - 1), + wrapItemAt(array, length - 2), + wrapItemAt(array, length - 3), length - 3); } else if (array.length() == 3) { return String.format(context.getString(R.string.notification_summary_medium), - wrapItemAt(array, 2, bidiFormatter), - wrapItemAt(array, 1, bidiFormatter), - wrapItemAt(array, 0, bidiFormatter)); + wrapItemAt(array, 2), + wrapItemAt(array, 1), + wrapItemAt(array, 0)); } else if (array.length() == 2) { return String.format(context.getString(R.string.notification_summary_small), - wrapItemAt(array, 1, bidiFormatter), - wrapItemAt(array, 0, bidiFormatter)); + wrapItemAt(array, 1), + wrapItemAt(array, 0)); } return null; } @Nullable - private static String titleForType(Context context, Notification notification, BidiFormatter bidiFormatter, AccountEntity account) { - String accountName = bidiFormatter.unicodeWrap(notification.getAccount().getName()); + private static String titleForType(Context context, Notification notification, AccountEntity account) { + String accountName = StringUtils.unicodeWrap(notification.getAccount().getName()); switch (notification.getType()) { case MENTION: return String.format(context.getString(R.string.notification_mention_format), diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationWorker.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationWorker.kt index 9abcd3ac..ae7d4d3f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationWorker.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationWorker.kt @@ -16,82 +16,35 @@ package com.keylesspalace.tusky.components.notifications import android.content.Context -import android.util.Log import androidx.work.ListenableWorker import androidx.work.Worker import androidx.work.WorkerFactory import androidx.work.WorkerParameters -import com.keylesspalace.tusky.db.AccountEntity -import com.keylesspalace.tusky.db.AccountManager -import com.keylesspalace.tusky.entity.Notification -import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.util.isLessThan -import java.io.IOException import javax.inject.Inject class NotificationWorker( - private val context: Context, + context: Context, params: WorkerParameters, - private val mastodonApi: MastodonApi, - private val accountManager: AccountManager + private val notificationsFetcher: NotificationFetcher ) : Worker(context, params) { override fun doWork(): Result { - val accountList = accountManager.getAllAccountsOrderedByActive() - for (account in accountList) { - if (account.notificationsEnabled) { - try { - Log.d(TAG, "getting Notifications for " + account.fullName) - val notificationsResponse = mastodonApi.notificationsWithAuth( - String.format("Bearer %s", account.accessToken), - account.domain - ).execute() - val notifications = notificationsResponse.body() - if (notificationsResponse.isSuccessful && notifications != null) { - onNotificationsReceived(account, notifications) - } else { - Log.w(TAG, "error receiving notifications") - } - } catch (e: IOException) { - Log.w(TAG, "error receiving notifications", e) - } - } - } + notificationsFetcher.fetchAndShow() return Result.success() } - - private fun onNotificationsReceived(account: AccountEntity, notificationList: List) { - val newId = account.lastNotificationId - var newestId = "" - var isFirstOfBatch = true - notificationList.reversed().forEach { notification -> - val currentId = notification.id - if (newestId.isLessThan(currentId)) { - newestId = currentId - } - if (newId.isLessThan(currentId)) { - NotificationHelper.make(context, notification, account, isFirstOfBatch) - isFirstOfBatch = false - } - } - account.lastNotificationId = newestId - accountManager.saveAccount(account) - } - - companion object { - private const val TAG = "NotificationWorker" - } - } class NotificationWorkerFactory @Inject constructor( - val api: MastodonApi, - val accountManager: AccountManager -): WorkerFactory() { + private val notificationsFetcher: NotificationFetcher +) : WorkerFactory() { - override fun createWorker(appContext: Context, workerClassName: String, workerParameters: WorkerParameters): ListenableWorker? { - if(workerClassName == NotificationWorker::class.java.name) { - return NotificationWorker(appContext, workerParameters, api, accountManager) + override fun createWorker( + appContext: Context, + workerClassName: String, + workerParameters: WorkerParameters + ): ListenableWorker? { + if (workerClassName == NotificationWorker::class.java.name) { + return NotificationWorker(appContext, workerParameters, notificationsFetcher) } return null } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/Notifier.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/Notifier.kt new file mode 100644 index 00000000..35c33a9b --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/Notifier.kt @@ -0,0 +1,20 @@ +package com.keylesspalace.tusky.components.notifications + +import android.content.Context +import com.keylesspalace.tusky.db.AccountEntity +import com.keylesspalace.tusky.entity.Notification + +/** + * Shows notifications. + */ +interface Notifier { + fun show(notification: Notification, account: AccountEntity, isFirstInBatch: Boolean) +} + +class SystemNotifier( + private val context: Context +) : Notifier { + override fun show(notification: Notification, account: AccountEntity, isFirstInBatch: Boolean) { + NotificationHelper.make(context, notification, account, isFirstInBatch) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/AccountPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt similarity index 99% rename from app/src/main/java/com/keylesspalace/tusky/fragment/preference/AccountPreferencesFragment.kt rename to app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt index 24049aa9..af882977 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/AccountPreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky.fragment.preference +package com.keylesspalace.tusky.components.preference import android.content.Intent import android.graphics.drawable.Drawable diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt new file mode 100644 index 00000000..c0e538a9 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt @@ -0,0 +1,258 @@ +package com.keylesspalace.tusky.components.preference + +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.widget.* +import androidx.appcompat.app.AlertDialog +import androidx.preference.Preference +import androidx.preference.PreferenceManager +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.SplashActivity +import com.keylesspalace.tusky.util.EmojiCompatFont +import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.FONTS +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import okhttp3.OkHttpClient +import kotlin.system.exitProcess + +/** + * This Preference lets the user select their preferred emoji font + */ +class EmojiPreference( + context: Context, + private val okHttpClient: OkHttpClient +) : Preference(context) { + + private lateinit var selected: EmojiCompatFont + private lateinit var original: EmojiCompatFont + private val radioButtons = mutableListOf() + private var updated = false + private var currentNeedsUpdate = false + + private val downloadDisposables = MutableList(FONTS.size) { null } + + override fun onAttachedToHierarchy(preferenceManager: PreferenceManager) { + super.onAttachedToHierarchy(preferenceManager) + + // Find out which font is currently active + selected = EmojiCompatFont.byId( + PreferenceManager.getDefaultSharedPreferences(context).getInt(key, 0) + ) + // We'll use this later to determine if anything has changed + original = selected + summary = selected.getDisplay(context) + } + + override fun onClick() { + val view = LayoutInflater.from(context).inflate(R.layout.dialog_emojicompat, null) + viewIds.forEachIndexed { index, viewId -> + setupItem(view.findViewById(viewId), FONTS[index]) + } + AlertDialog.Builder(context) + .setView(view) + .setPositiveButton(android.R.string.ok) { _, _ -> onDialogOk() } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + private fun setupItem(container: View, font: EmojiCompatFont) { + val title: TextView = container.findViewById(R.id.emojicompat_name) + val caption: TextView = container.findViewById(R.id.emojicompat_caption) + val thumb: ImageView = container.findViewById(R.id.emojicompat_thumb) + val download: ImageButton = container.findViewById(R.id.emojicompat_download) + val cancel: ImageButton = container.findViewById(R.id.emojicompat_download_cancel) + val radio: RadioButton = container.findViewById(R.id.emojicompat_radio) + + // Initialize all the views + title.text = font.getDisplay(container.context) + caption.setText(font.caption) + thumb.setImageResource(font.img) + + // There needs to be a list of all the radio buttons in order to uncheck them when one is selected + radioButtons.add(radio) + updateItem(font, container) + + // Set actions + download.setOnClickListener { startDownload(font, container) } + cancel.setOnClickListener { cancelDownload(font, container) } + radio.setOnClickListener { radioButton: View -> select(font, radioButton as RadioButton) } + container.setOnClickListener { containerView: View -> + select(font, containerView.findViewById(R.id.emojicompat_radio)) + } + } + + private fun startDownload(font: EmojiCompatFont, container: View) { + val download: ImageButton = container.findViewById(R.id.emojicompat_download) + val caption: TextView = container.findViewById(R.id.emojicompat_caption) + val progressBar: ProgressBar = container.findViewById(R.id.emojicompat_progress) + val cancel: ImageButton = container.findViewById(R.id.emojicompat_download_cancel) + + // Switch to downloading style + download.visibility = View.GONE + caption.visibility = View.INVISIBLE + progressBar.visibility = View.VISIBLE + progressBar.progress = 0 + cancel.visibility = View.VISIBLE + font.downloadFontFile(context, okHttpClient) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { progress -> + // The progress is returned as a float between 0 and 1, or -1 if it could not determined + if (progress >= 0) { + progressBar.isIndeterminate = false + val max = progressBar.max.toFloat() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + progressBar.setProgress((max * progress).toInt(), true) + } else { + progressBar.progress = (max * progress).toInt() + } + } else { + progressBar.isIndeterminate = true + } + }, + { + Toast.makeText(context, R.string.download_failed, Toast.LENGTH_SHORT).show() + updateItem(font, container) + }, + { + finishDownload(font, container) + } + ).also { downloadDisposables[font.id] = it } + + + } + + private fun cancelDownload(font: EmojiCompatFont, container: View) { + font.deleteDownloadedFile(container.context) + downloadDisposables[font.id]?.dispose() + downloadDisposables[font.id] = null + updateItem(font, container) + } + + private fun finishDownload(font: EmojiCompatFont, container: View) { + select(font, container.findViewById(R.id.emojicompat_radio)) + updateItem(font, container) + // Set the flag to restart the app (because an update has been downloaded) + if (selected === original && currentNeedsUpdate) { + updated = true + currentNeedsUpdate = false + } + } + + /** + * Select a font both visually and logically + * + * @param font The font to be selected + * @param radio The radio button associated with it's visual item + */ + private fun select(font: EmojiCompatFont, radio: RadioButton) { + selected = font + // Uncheck all the other buttons + for (other in radioButtons) { + if (other !== radio) { + other.isChecked = false + } + } + radio.isChecked = true + } + + /** + * Called when a "consistent" state is reached, i.e. it's not downloading the font + * + * @param font The font to be displayed + * @param container The ConstraintLayout containing the item + */ + private fun updateItem(font: EmojiCompatFont, container: View) { + // Assignments + val download: ImageButton = container.findViewById(R.id.emojicompat_download) + val caption: TextView = container.findViewById(R.id.emojicompat_caption) + val progress: ProgressBar = container.findViewById(R.id.emojicompat_progress) + val cancel: ImageButton = container.findViewById(R.id.emojicompat_download_cancel) + val radio: RadioButton = container.findViewById(R.id.emojicompat_radio) + + // There's no download going on + progress.visibility = View.GONE + cancel.visibility = View.GONE + caption.visibility = View.VISIBLE + if (font.isDownloaded(context)) { + // Make it selectable + download.visibility = View.GONE + radio.visibility = View.VISIBLE + container.isClickable = true + } else { + // Make it downloadable + download.visibility = View.VISIBLE + radio.visibility = View.GONE + container.isClickable = false + } + + // Select it if necessary + if (font === selected) { + radio.isChecked = true + // Update available + if (!font.isDownloaded(context)) { + currentNeedsUpdate = true + } + } else { + radio.isChecked = false + } + } + + private fun saveSelectedFont() { + val index = selected.id + Log.i(TAG, "saveSelectedFont: Font ID: $index") + PreferenceManager + .getDefaultSharedPreferences(context) + .edit() + .putInt(key, index) + .apply() + summary = selected.getDisplay(context) + } + + /** + * User clicked ok -> save the selected font and offer to restart the app if something changed + */ + private fun onDialogOk() { + saveSelectedFont() + if (selected !== original || updated) { + AlertDialog.Builder(context) + .setTitle(R.string.restart_required) + .setMessage(R.string.restart_emoji) + .setNegativeButton(R.string.later, null) + .setPositiveButton(R.string.restart) { _, _ -> + // Restart the app + // From https://stackoverflow.com/a/17166729/5070653 + val launchIntent = Intent(context, SplashActivity::class.java) + val mPendingIntent = PendingIntent.getActivity( + context, + 0x1f973, // This is the codepoint of the party face emoji :D + launchIntent, + PendingIntent.FLAG_CANCEL_CURRENT) + val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + mgr.set( + AlarmManager.RTC, + System.currentTimeMillis() + 100, + mPendingIntent) + exitProcess(0) + }.show() + } + } + + companion object { + private const val TAG = "EmojiPreference" + + // Please note that this array must sorted in the same way as the fonts. + private val viewIds = intArrayOf( + R.id.item_nomoji, + R.id.item_blobmoji, + R.id.item_twemoji, + R.id.item_notoemoji + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/NotificationPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt similarity index 99% rename from app/src/main/java/com/keylesspalace/tusky/fragment/preference/NotificationPreferencesFragment.kt rename to app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt index 800ea7c1..f917d2bb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/NotificationPreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky.fragment.preference +package com.keylesspalace.tusky.components.preference import android.os.Bundle import androidx.preference.PreferenceFragmentCompat diff --git a/app/src/main/java/com/keylesspalace/tusky/PreferencesActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt similarity index 97% rename from app/src/main/java/com/keylesspalace/tusky/PreferencesActivity.kt rename to app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt index 383a07ae..4fe0abd8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/PreferencesActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky +package com.keylesspalace.tusky.components.preference import android.content.Context import android.content.Intent @@ -23,9 +23,11 @@ import android.util.Log import android.view.MenuItem import androidx.fragment.app.Fragment import androidx.preference.PreferenceManager +import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.MainActivity +import com.keylesspalace.tusky.R import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.PreferenceChangedEvent -import com.keylesspalace.tusky.fragment.preference.* import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.getNonNullString diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/PreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt similarity index 96% rename from app/src/main/java/com/keylesspalace/tusky/fragment/preference/PreferencesFragment.kt rename to app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt index 6dec7273..edddffca 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/PreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt @@ -13,13 +13,13 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky.fragment.preference +package com.keylesspalace.tusky.components.preference import android.os.Bundle import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat -import com.keylesspalace.tusky.PreferencesActivity import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.settings.* import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.getNonNullString @@ -27,8 +27,13 @@ import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizePx +import okhttp3.OkHttpClient +import javax.inject.Inject -class PreferencesFragment : PreferenceFragmentCompat() { +class PreferencesFragment : PreferenceFragmentCompat(), Injectable { + + @Inject + lateinit var okhttpclient: OkHttpClient private val iconSize by lazy { resources.getDimensionPixelSize(R.dimen.preference_icon_size) } private var httpProxyPref: Preference? = null @@ -47,7 +52,7 @@ class PreferencesFragment : PreferenceFragmentCompat() { icon = makeIcon(GoogleMaterial.Icon.gmd_palette) } - emojiPreference { + emojiPreference(okhttpclient) { setDefaultValue("system_default") setIcon(R.drawable.ic_emoji_24dp) key = PrefKeys.EMOJI diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/ProxyPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/ProxyPreferencesFragment.kt similarity index 97% rename from app/src/main/java/com/keylesspalace/tusky/fragment/preference/ProxyPreferencesFragment.kt rename to app/src/main/java/com/keylesspalace/tusky/components/preference/ProxyPreferencesFragment.kt index e7ee7ade..922d5a7a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/ProxyPreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/ProxyPreferencesFragment.kt @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky.fragment.preference +package com.keylesspalace.tusky.components.preference import android.os.Bundle import androidx.preference.PreferenceFragmentCompat diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/TabFilterPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/TabFilterPreferencesFragment.kt similarity index 97% rename from app/src/main/java/com/keylesspalace/tusky/fragment/preference/TabFilterPreferencesFragment.kt rename to app/src/main/java/com/keylesspalace/tusky/components/preference/TabFilterPreferencesFragment.kt index cd76300d..71c5e10e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/TabFilterPreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/TabFilterPreferencesFragment.kt @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky.fragment.preference +package com.keylesspalace.tusky.components.preference import android.os.Bundle import androidx.preference.PreferenceFragmentCompat diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt index 5daf584b..3ecadd58 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt @@ -20,7 +20,6 @@ import android.content.Intent import android.os.Bundle import android.view.MenuItem import androidx.activity.viewModels -import androidx.lifecycle.Observer import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.report.adapter.ReportPagerAdapter @@ -77,7 +76,7 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector { } private fun subscribeObservables() { - viewModel.navigation.observe(this, Observer { screen -> + viewModel.navigation.observe(this) { screen -> if (screen != null) { viewModel.navigated() when (screen) { @@ -88,14 +87,14 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector { Screen.Finish -> closeScreen() } } - }) + } - viewModel.checkUrl.observe(this, Observer { + viewModel.checkUrl.observe(this) { if (!it.isNullOrBlank()) { viewModel.urlChecked() viewUrl(it) } - }) + } } private fun showPreviousScreen() { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt index 8b1e5cda..fdc73163 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt @@ -19,6 +19,9 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Transformations import androidx.paging.PagedList +import com.keylesspalace.tusky.appstore.BlockEvent +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.MuteEvent import com.keylesspalace.tusky.components.report.adapter.StatusesRepository import com.keylesspalace.tusky.components.report.model.StatusViewState import com.keylesspalace.tusky.entity.Relationship @@ -31,6 +34,7 @@ import javax.inject.Inject class ReportViewModel @Inject constructor( private val mastodonApi: MastodonApi, + private val eventHub: EventHub, private val statusesRepository: StatusesRepository) : RxAwareViewModel() { private val navigationMutable = MutableLiveData() @@ -96,7 +100,7 @@ class ReportViewModel @Inject constructor( val ids = listOf(accountId) muteStateMutable.value = Loading() blockStateMutable.value = Loading() - mastodonApi.relationshipsObservable(ids) + mastodonApi.relationships(ids) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( @@ -123,16 +127,21 @@ class ReportViewModel @Inject constructor( } fun toggleMute() { - if (muteStateMutable.value?.data == true) { - mastodonApi.unmuteAccountObservable(accountId) + val alreadyMuted = muteStateMutable.value?.data == true + if (alreadyMuted) { + mastodonApi.unmuteAccount(accountId) } else { - mastodonApi.muteAccountObservable(accountId) + mastodonApi.muteAccount(accountId) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( { relationship -> - muteStateMutable.value = Success(relationship?.muting == true) + val muting = relationship?.muting == true + muteStateMutable.value = Success(muting) + if (muting) { + eventHub.dispatch(MuteEvent(accountId)) + } }, { error -> muteStateMutable.value = Error(false, error.message) @@ -143,16 +152,21 @@ class ReportViewModel @Inject constructor( } fun toggleBlock() { - if (blockStateMutable.value?.data == true) { - mastodonApi.unblockAccountObservable(accountId) + val alreadyBlocked = blockStateMutable.value?.data == true + if (alreadyBlocked) { + mastodonApi.unblockAccount(accountId) } else { - mastodonApi.blockAccountObservable(accountId) + mastodonApi.blockAccount(accountId) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( { relationship -> - blockStateMutable.value = Success(relationship?.blocking == true) + val blocking = relationship?.blocking == true + blockStateMutable.value = Success(blocking) + if (blocking) { + eventHub.dispatch(BlockEvent(accountId)) + } }, { error -> blockStateMutable.value = Error(false, error.message) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/AdapterHandler.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/AdapterHandler.kt index 588cfef0..957d5b32 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/AdapterHandler.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/AdapterHandler.kt @@ -16,10 +16,8 @@ package com.keylesspalace.tusky.components.report.adapter import android.view.View -import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.interfaces.LinkListener -import java.util.ArrayList interface AdapterHandler: LinkListener { fun showMedia(v: View?, status: Status?, idx: Int) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesRepository.kt index 852a07f9..cea3080e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesRepository.kt @@ -21,7 +21,6 @@ import androidx.paging.toLiveData import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.BiListing -import com.keylesspalace.tusky.util.Listing import io.reactivex.disposables.CompositeDisposable import java.util.concurrent.Executors import javax.inject.Inject diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt index 611231ec..6151903f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt @@ -22,7 +22,6 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels -import androidx.lifecycle.Observer import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.report.ReportViewModel import com.keylesspalace.tusky.components.report.Screen @@ -55,7 +54,7 @@ class ReportDoneFragment : Fragment(), Injectable { } private fun subscribeObservables() { - viewModel.muteState.observe(viewLifecycleOwner, Observer { + viewModel.muteState.observe(viewLifecycleOwner) { if (it !is Loading) { buttonMute.show() progressMute.show() @@ -68,9 +67,9 @@ class ReportDoneFragment : Fragment(), Injectable { true -> R.string.action_unmute else -> R.string.action_mute }) - }) + } - viewModel.blockState.observe(viewLifecycleOwner, Observer { + viewModel.blockState.observe(viewLifecycleOwner) { if (it !is Loading) { buttonBlock.show() progressBlock.show() @@ -83,7 +82,7 @@ class ReportDoneFragment : Fragment(), Injectable { true -> R.string.action_unblock else -> R.string.action_block }) - }) + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt index 5a94ba17..4e7b00ab 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt @@ -22,7 +22,6 @@ import android.view.ViewGroup import androidx.core.widget.doAfterTextChanged import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels -import androidx.lifecycle.Observer import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.report.ReportViewModel @@ -81,14 +80,14 @@ class ReportNoteFragment : Fragment(), Injectable { } private fun subscribeObservables() { - viewModel.reportingState.observe(viewLifecycleOwner, Observer { + viewModel.reportingState.observe(viewLifecycleOwner) { when (it) { is Success -> viewModel.navigateTo(Screen.Done) is Loading -> showLoading() is Error -> showError(it.cause) } - }) + } } private fun showError(error: Throwable?) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt index e5308efc..703acee2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt @@ -23,8 +23,6 @@ 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.paging.PagedList import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager @@ -43,7 +41,10 @@ import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Status -import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.util.CardViewMode +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.viewdata.AttachmentViewData import kotlinx.android.synthetic.main.fragment_report_statuses.* import javax.inject.Inject @@ -129,11 +130,11 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler { recyclerView.adapter = adapter (recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false - viewModel.statuses.observe(viewLifecycleOwner, Observer> { + viewModel.statuses.observe(viewLifecycleOwner) { adapter.submitList(it) - }) + } - viewModel.networkStateAfter.observe(viewLifecycleOwner, Observer { + viewModel.networkStateAfter.observe(viewLifecycleOwner) { if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING) progressBarBottom.show() else @@ -141,9 +142,9 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler { if (it?.status == com.keylesspalace.tusky.util.Status.FAILED) showError(it.msg) - }) + } - viewModel.networkStateBefore.observe(viewLifecycleOwner, Observer { + viewModel.networkStateBefore.observe(viewLifecycleOwner) { if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING) progressBarTop.show() else @@ -151,9 +152,9 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler { if (it?.status == com.keylesspalace.tusky.util.Status.FAILED) showError(it.msg) - }) + } - viewModel.networkStateRefresh.observe(viewLifecycleOwner, Observer { + viewModel.networkStateRefresh.observe(viewLifecycleOwner) { if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING && !swipeRefreshLayout.isRefreshing) progressBarLoading.show() else @@ -163,7 +164,7 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler { swipeRefreshLayout.isRefreshing = false if (it?.status == com.keylesspalace.tusky.util.Status.FAILED) showError(it.msg) - }) + } } private fun showError(@Suppress("UNUSED_PARAMETER") msg: String?) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt index 2ade20b8..18b04df5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt @@ -19,7 +19,6 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.view.MenuItem -import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager @@ -30,7 +29,6 @@ import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.ScheduledStatus import com.keylesspalace.tusky.util.Status -import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show import kotlinx.android.synthetic.main.activity_scheduled_toot.* @@ -68,11 +66,11 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec viewModel = ViewModelProvider(this, viewModelFactory)[ScheduledTootViewModel::class.java] - viewModel.data.observe(this, Observer { + viewModel.data.observe(this) { adapter.submitList(it) - }) + } - viewModel.networkState.observe(this, Observer { (status) -> + viewModel.networkState.observe(this) { (status) -> when(status) { Status.SUCCESS -> { progressBar.hide() @@ -103,9 +101,7 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec } } } - - }) - + } } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -124,6 +120,7 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec override fun edit(item: ScheduledStatus) { val intent = ComposeActivity.startIntent(this, ComposeActivity.ComposeOptions( + scheduledTootUid = item.id, tootText = item.params.text, contentWarning = item.params.spoilerText, mediaAttachments = item.mediaAttachments, diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt index c185bcc3..c5929c43 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt @@ -7,7 +7,6 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer import androidx.paging.PagedList import androidx.paging.PagedListAdapter import androidx.recyclerview.widget.DividerItemDecoration @@ -61,11 +60,11 @@ abstract class SearchFragment : Fragment(), } private fun subscribeObservables() { - data.observe(viewLifecycleOwner, Observer { + data.observe(viewLifecycleOwner) { adapter.submitList(it) - }) + } - networkStateRefresh.observe(viewLifecycleOwner, Observer { + networkStateRefresh.observe(viewLifecycleOwner) { searchProgressBar.visible(it == NetworkState.LOADING) @@ -73,17 +72,16 @@ abstract class SearchFragment : Fragment(), showError() } checkNoData() + } - }) - - networkState.observe(viewLifecycleOwner, Observer { + networkState.observe(viewLifecycleOwner) { progressBarBottom.visible(it == NetworkState.LOADING) if (it.status == Status.FAILED) { showError() } - }) + } } private fun checkNoData() { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt index 2a920881..ce5c2612 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt @@ -26,8 +26,6 @@ import android.net.Uri import android.os.Environment import android.util.Log import android.view.View -import android.widget.CheckBox -import android.widget.TextView import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.PopupMenu @@ -376,9 +374,10 @@ class SearchStatusesFragment : SearchFragment viewModel.muteAccount(accountId, notifications) } - ) + accountUsername + ) { notifications -> + viewModel.muteAccount(accountId, notifications) + } } private fun accountIsInMentions(account: AccountEntity?, mentions: Array): Boolean { diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt index 5293bd03..138f71a3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt @@ -21,7 +21,6 @@ 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 @@ -63,7 +62,7 @@ class AccountManager @Inject constructor(db: AppDatabase) { accountDao.insertOrReplace(it) } - val maxAccountId = accounts.maxBy { it.id }?.id ?: 0 + val maxAccountId = accounts.maxByOrNull { it.id }?.id ?: 0 val newAccountId = maxAccountId + 1 activeAccount = AccountEntity(id = newAccountId, domain = domain.toLowerCase(Locale.ROOT), accessToken = accessToken, isActive = true) @@ -166,13 +165,13 @@ class AccountManager @Inject constructor(db: AppDatabase) { */ fun getAllAccountsOrderedByActive(): List { val accountsCopy = accounts.toMutableList() - accountsCopy.sortWith(Comparator { l, r -> + accountsCopy.sortWith { l, r -> when { l.isActive && !r.isActive -> -1 r.isActive && !l.isActive -> 1 else -> 0 } - }) + } return accountsCopy } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt index 35ca6fc9..9c50ad03 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt @@ -99,9 +99,8 @@ WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = AND serverId = :statusId""") abstract fun delete(accountId: Long, statusId: String) - @Query("""DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId -AND authorServerId != :accountServerId AND createdAt < :olderThan""") - abstract fun cleanup(accountId: Long, accountServerId: String, olderThan: Long) + @Query("""DELETE FROM TimelineStatusEntity WHERE createdAt < :olderThan""") + abstract fun cleanup(olderThan: Long) @Query("""UPDATE TimelineStatusEntity SET poll = :poll WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)""") diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt index 1f958e5a..0257c28f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt @@ -16,8 +16,10 @@ package com.keylesspalace.tusky.di import com.keylesspalace.tusky.* +import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.instancemute.InstanceListActivity +import com.keylesspalace.tusky.components.preference.PreferencesActivity import com.keylesspalace.tusky.components.report.ReportActivity import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity import com.keylesspalace.tusky.components.search.SearchActivity @@ -102,4 +104,7 @@ abstract class ActivitiesModule { @ContributesAndroidInjector abstract fun contributesScheduledTootActivity(): ScheduledTootActivity + + @ContributesAndroidInjector + abstract fun contributesAnnouncementsActivity(): AnnouncementsActivity } diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt index e87ef289..aedcc3ae 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt @@ -25,6 +25,8 @@ 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.components.notifications.Notifier +import com.keylesspalace.tusky.components.notifications.SystemNotifier import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.TimelineCases @@ -79,7 +81,11 @@ class AppModule { AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19, AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22, AppDatabase.MIGRATION_22_23) - .build() + .build() } + @Provides + @Singleton + fun notifier(context: Context): Notifier = SystemNotifier(context) + } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt index 48e8b625..a1a8c8fe 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt @@ -20,14 +20,15 @@ import com.keylesspalace.tusky.AccountsInListFragment import com.keylesspalace.tusky.components.conversation.ConversationsFragment import com.keylesspalace.tusky.components.instancemute.fragment.InstanceListFragment import com.keylesspalace.tusky.fragment.* -import com.keylesspalace.tusky.fragment.preference.AccountPreferencesFragment -import com.keylesspalace.tusky.fragment.preference.NotificationPreferencesFragment +import com.keylesspalace.tusky.components.preference.AccountPreferencesFragment +import com.keylesspalace.tusky.components.preference.NotificationPreferencesFragment import com.keylesspalace.tusky.components.report.fragments.ReportDoneFragment import com.keylesspalace.tusky.components.report.fragments.ReportNoteFragment import com.keylesspalace.tusky.components.report.fragments.ReportStatusesFragment import com.keylesspalace.tusky.components.search.fragments.SearchAccountsFragment import com.keylesspalace.tusky.components.search.fragments.SearchHashtagsFragment import com.keylesspalace.tusky.components.search.fragments.SearchStatusesFragment +import com.keylesspalace.tusky.components.preference.PreferencesFragment import dagger.Module import dagger.android.ContributesAndroidInjector @@ -85,4 +86,7 @@ abstract class FragmentBuildersModule { @ContributesAndroidInjector abstract fun searchHashtagsFragment(): SearchHashtagsFragment + @ContributesAndroidInjector + abstract fun preferencesFragment(): PreferencesFragment + } diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt b/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt index f4929463..c461012d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt @@ -4,6 +4,7 @@ package com.keylesspalace.tusky.di import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import com.keylesspalace.tusky.components.announcements.AnnouncementsViewModel import com.keylesspalace.tusky.components.compose.ComposeViewModel import com.keylesspalace.tusky.components.conversation.ConversationsViewModel import com.keylesspalace.tusky.components.report.ReportViewModel @@ -85,5 +86,10 @@ abstract class ViewModelModule { @ViewModelKey(ScheduledTootViewModel::class) internal abstract fun scheduledTootViewModel(viewModel: ScheduledTootViewModel): ViewModel + @Binds + @IntoMap + @ViewModelKey(AnnouncementsViewModel::class) + internal abstract fun announcementsViewModel(viewModel: AnnouncementsViewModel): ViewModel + //Add more ViewModels here -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt new file mode 100644 index 00000000..5cd32fe8 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt @@ -0,0 +1,57 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.entity + +import android.text.Spanned +import com.google.gson.annotations.SerializedName +import java.util.* + +data class Announcement( + val id: String, + val content: Spanned, + @SerializedName("starts_at") val startsAt: Date?, + @SerializedName("ends_at") val endsAt: Date?, + @SerializedName("all_day") val allDay: Boolean, + @SerializedName("published_at") val publishedAt: Date, + @SerializedName("updated_at") val updatedAt: Date, + val read: Boolean, + val mentions: List, + val statuses: List, + val tags: List, + val emojis: List, + val reactions: List +) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || javaClass != other.javaClass) return false + + val announcement = other as Announcement? + return id == announcement?.id + } + + override fun hashCode(): Int { + return id.hashCode() + } + + data class Reaction( + val name: String, + var count: Int, + var me: Boolean, + val url: String?, + @SerializedName("static_url") val staticUrl: String? + ) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt index 43e54f01..ada9ec20 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt @@ -26,7 +26,9 @@ data class Card( val image: String, val type: String, val width: Int, - val height: Int + val height: Int, + val blurhash: String?, + val embed_url: String? ) { override fun hashCode(): Int { @@ -41,4 +43,7 @@ data class Card( return account?.url == this.url } + companion object { + const val TYPE_PHOTO = "photo" + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt index baee54bc..fe7a22c7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt @@ -23,5 +23,6 @@ import kotlinx.android.parcel.Parcelize data class Emoji( val shortcode: String, val url: String, + @SerializedName("static_url") val staticUrl: String, @SerializedName("visible_in_picker") val visibleInPicker: Boolean? -) : Parcelable \ No newline at end of file +) : Parcelable diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Marker.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Marker.kt new file mode 100644 index 00000000..16fd9e31 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Marker.kt @@ -0,0 +1,15 @@ +package com.keylesspalace.tusky.entity + +import com.google.gson.annotations.SerializedName +import java.util.* + +/** + * API type for saving the scroll position of a timeline. + */ +data class Marker( + @SerializedName("last_read_id") + val lastReadId: String, + val version: Int, + @SerializedName("updated_at") + val updatedAt: Date +) \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.kt index 0c974d19..e6a75c51 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.kt @@ -26,5 +26,6 @@ data class Relationship ( @SerializedName("muting_notifications") val mutingNotifications: Boolean, val requested: Boolean, @SerializedName("showing_reblogs") val showingReblogs: Boolean, - @SerializedName("domain_blocking") val blockingDomain: Boolean + @SerializedName("domain_blocking") val blockingDomain: Boolean, + val note: String? // nullable for backward compatibility / feature detection ) diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt index 9ec53b2d..d15835e3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt @@ -52,7 +52,6 @@ import java.io.IOException import java.util.HashMap import javax.inject.Inject - class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { @Inject @@ -116,27 +115,17 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { } override fun onMute(mute: Boolean, id: String, position: Int, notifications: Boolean) { - val callback = object : Callback { - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - onMuteSuccess(mute, id, position, notifications) - } else { - onMuteFailure(mute, id, notifications) - } - } - - override fun onFailure(call: Call, t: Throwable) { - onMuteFailure(mute, id, notifications) - } - } - - val call = if (!mute) { + if (!mute) { api.unmuteAccount(id) } else { api.muteAccount(id, notifications) } - callList.add(call) - call.enqueue(callback) + .autoDispose(from(this)) + .subscribe({ + onMuteSuccess(mute, id, position, notifications) + }, { + onMuteFailure(mute, id, notifications) + }) } private fun onMuteSuccess(muted: Boolean, id: String, position: Int, notifications: Boolean) { @@ -171,27 +160,17 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { } override fun onBlock(block: Boolean, id: String, position: Int) { - val cb = object : Callback { - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - onBlockSuccess(block, id, position) - } else { - onBlockFailure(block, id) - } - } - - override fun onFailure(call: Call, t: Throwable) { - onBlockFailure(block, id) - } - } - - val call = if (!block) { + if (!block) { api.unblockAccount(id) } else { api.blockAccount(id) } - callList.add(call) - call.enqueue(cb) + .autoDispose(from(this)) + .subscribe({ + onBlockSuccess(block, id, position) + }, { + onBlockFailure(block, id) + }) } private fun onBlockSuccess(blocked: Boolean, id: String, position: Int) { @@ -350,29 +329,16 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { } private fun fetchRelationships(ids: List) { - val callback = object : Callback> { - override fun onResponse(call: Call>, response: Response>) { - val body = response.body() - if (response.isSuccessful && body != null) { - onFetchRelationshipsSuccess(body) - } else { + api.relationships(ids) + .autoDispose(from(this)) + .subscribe(::onFetchRelationshipsSuccess) { onFetchRelationshipsFailure(ids) } - } - - override fun onFailure(call: Call>, t: Throwable) { - onFetchRelationshipsFailure(ids) - } - } - - val call = api.relationships(ids) - callList.add(call) - call.enqueue(callback) } private fun onFetchRelationshipsSuccess(relationships: List) { val mutesAdapter = adapter as MutesAdapter - var mutingNotificationsMap = HashMap() + val mutingNotificationsMap = HashMap() relationships.map { mutingNotificationsMap.put(it.id, it.mutingNotifications) } mutesAdapter.updateMutingNotificationsMap(mutingNotificationsMap) } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java index 3f84b3a6..8fe141c6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java @@ -457,6 +457,7 @@ public abstract class SFragment extends BaseFragment implements Injectable { composeOptions.setContentWarning(deletedStatus.getSpoilerText()); composeOptions.setMediaAttachments(deletedStatus.getAttachments()); composeOptions.setSensitive(deletedStatus.getSensitive()); + composeOptions.setModifiedInitialState(true); if (deletedStatus.getPoll() != null) { composeOptions.setPoll(deletedStatus.getPoll().toNewPoll(deletedStatus.getCreatedAt())); } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java index 71cc1181..ddcb0eb0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java @@ -392,6 +392,7 @@ public class TimelineFragment extends SFragment implements // home, notifications, public, thread switch (kind) { case HOME: + case LIST: return filterContext.contains(Filter.HOME); case PUBLIC_FEDERATED: case PUBLIC_LOCAL: @@ -1209,7 +1210,7 @@ public class TimelineFragment extends SFragment implements if (status != null && ((status.getInReplyToId() != null && filterRemoveReplies) || (status.getReblog() != null && filterRemoveReblogs) - || shouldFilterStatus(status))) { + || shouldFilterStatus(status.getActionableStatus()))) { it.remove(); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt index e61a24e5..fc4dfcb9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt @@ -99,9 +99,9 @@ class ViewImageFragment : ViewMediaFragment() { url = attachment.url description = attachment.description } else { - url = arguments.getString(ARG_AVATAR_URL) + url = arguments.getString(ARG_SINGLE_IMAGE_URL) if (url == null) { - throw IllegalArgumentException("attachment or avatar url has to be set") + throw IllegalArgumentException("attachment or image url has to be set") } } @@ -158,6 +158,9 @@ class ViewImageFragment : ViewMediaFragment() { } private fun onGestureEnd() { + if (photoView == null) { + return + } if (abs(photoView.translationY) > 180) { photoActionsListener.onDismiss() } else { diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt index 9b51f285..86b3d09b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt @@ -42,7 +42,7 @@ abstract class ViewMediaFragment : BaseFragment() { @JvmStatic protected val ARG_ATTACHMENT = "attach" @JvmStatic - protected val ARG_AVATAR_URL = "avatarUrl" + protected val ARG_SINGLE_IMAGE_URL = "singleImageUrl" @JvmStatic fun newInstance(attachment: Attachment, shouldStartPostponedTransition: Boolean): ViewMediaFragment { @@ -62,10 +62,10 @@ abstract class ViewMediaFragment : BaseFragment() { } @JvmStatic - fun newAvatarInstance(avatarUrl: String): ViewMediaFragment { + fun newSingleImageInstance(imageUrl: String): ViewMediaFragment { val arguments = Bundle(2) val fragment = ViewImageFragment() - arguments.putString(ARG_AVATAR_URL, avatarUrl) + arguments.putString(ARG_SINGLE_IMAGE_URL, imageUrl) arguments.putBoolean(ARG_START_POSTPONED_TRANSITION, true) fragment.arguments = arguments diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt index 654dd40c..33d8f192 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt @@ -189,7 +189,7 @@ class ViewVideoFragment : ViewMediaFragment() { mediaDescription.animate().alpha(alpha) .setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { - mediaDescription.visible(isDescriptionVisible) + mediaDescription?.visible(isDescriptionVisible) animation.removeListener(this) } }) diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt index 767fe257..8f3dab3f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -99,11 +99,19 @@ interface MastodonApi { @Query("exclude_types[]") excludes: Set? ): Call> + @GET("api/v1/markers") + fun markersWithAuth( + @Header("Authorization") auth: String, + @Header(DOMAIN_HEADER) domain: String, + @Query("timeline[]") timelines: List + ): Single> + @GET("api/v1/notifications") fun notificationsWithAuth( @Header("Authorization") auth: String, - @Header(DOMAIN_HEADER) domain: String - ): Call> + @Header(DOMAIN_HEADER) domain: String, + @Query("since_id") sinceId: String? + ): Single> @POST("api/v1/notifications/clear") fun clearNotifications(): Call @@ -261,7 +269,7 @@ interface MastodonApi { @GET("api/v1/accounts/{id}") fun account( @Path("id") accountId: String - ): Call + ): Single /** * Method to fetch statuses for the specified account. @@ -300,44 +308,44 @@ interface MastodonApi { fun followAccount( @Path("id") accountId: String, @Field("reblogs") showReblogs: Boolean - ): Call + ): Single @POST("api/v1/accounts/{id}/unfollow") fun unfollowAccount( @Path("id") accountId: String - ): Call + ): Single @POST("api/v1/accounts/{id}/block") fun blockAccount( @Path("id") accountId: String - ): Call + ): Single @POST("api/v1/accounts/{id}/unblock") fun unblockAccount( @Path("id") accountId: String - ): Call + ): Single @FormUrlEncoded @POST("api/v1/accounts/{id}/mute") fun muteAccount( @Path("id") accountId: String, - @Field("notifications") notifications: Boolean - ): Call + @Field("notifications") notifications: Boolean? = null + ): Single @POST("api/v1/accounts/{id}/unmute") fun unmuteAccount( @Path("id") accountId: String - ): Call + ): Single @GET("api/v1/accounts/relationships") fun relationships( @Query("id[]") accountIds: List - ): Call> + ): Single> @GET("api/v1/accounts/{id}/identity_proofs") fun identityProofs( @Path("id") accountId: String - ): Call> + ): Single> @GET("api/v1/blocks") fun blocks( @@ -505,30 +513,27 @@ interface MastodonApi { @Field("choices[]") choices: List ): Single - @POST("api/v1/accounts/{id}/block") - fun blockAccountObservable( - @Path("id") accountId: String - ): Single + @GET("api/v1/announcements") + fun listAnnouncements( + @Query("with_dismissed") withDismissed: Boolean = true + ): Single> - @POST("api/v1/accounts/{id}/unblock") - fun unblockAccountObservable( - @Path("id") accountId: String - ): Single + @POST("api/v1/announcements/{id}/dismiss") + fun dismissAnnouncement( + @Path("id") announcementId: String + ): Single - @POST("api/v1/accounts/{id}/mute") - fun muteAccountObservable( - @Path("id") accountId: String - ): Single + @PUT("api/v1/announcements/{id}/reactions/{name}") + fun addAnnouncementReaction( + @Path("id") announcementId: String, + @Path("name") name: String + ): Single - @POST("api/v1/accounts/{id}/unmute") - fun unmuteAccountObservable( - @Path("id") accountId: String - ): Single - - @GET("api/v1/accounts/relationships") - fun relationshipsObservable( - @Query("id[]") accountIds: List - ): Single> + @DELETE("api/v1/announcements/{id}/reactions/{name}") + fun removeAnnouncementReaction( + @Path("id") announcementId: String, + @Path("name") name: String + ): Single @FormUrlEncoded @POST("api/v1/reports") @@ -563,4 +568,11 @@ interface MastodonApi { @Query("following") following: Boolean? = null ): Single + @FormUrlEncoded + @POST("api/v1/accounts/{id}/note") + fun updateAccountNote( + @Path("id") accountId: String, + @Field("comment") note: String + ): Single + } diff --git a/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt b/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt index 5b295f98..efdb410b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt @@ -15,17 +15,14 @@ package com.keylesspalace.tusky.network +import android.util.Log import com.keylesspalace.tusky.appstore.* import com.keylesspalace.tusky.entity.DeletedStatus import com.keylesspalace.tusky.entity.Poll -import com.keylesspalace.tusky.entity.Relationship import com.keylesspalace.tusky.entity.Status import io.reactivex.Single import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.addTo -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response import java.lang.IllegalStateException /** @@ -108,24 +105,23 @@ class TimelineCasesImpl( } override fun mute(id: String, notifications: Boolean) { - val call = mastodonApi.muteAccount(id, notifications) - call.enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) {} - - override fun onFailure(call: Call, t: Throwable) {} - }) - eventHub.dispatch(MuteEvent(id)) + mastodonApi.muteAccount(id, notifications) + .subscribe({ + eventHub.dispatch(MuteEvent(id)) + }, { t -> + Log.w("Failed to mute account", t) + }) + .addTo(cancelDisposable) } override fun block(id: String) { - val call = mastodonApi.blockAccount(id) - call.enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) {} - - override fun onFailure(call: Call, t: Throwable) {} - }) - eventHub.dispatch(BlockEvent(id)) - + mastodonApi.blockAccount(id) + .subscribe({ + eventHub.dispatch(BlockEvent(id)) + }, { t -> + Log.w("Failed to block account", t) + }) + .addTo(cancelDisposable) } override fun delete(id: String): Single { diff --git a/app/src/main/java/com/keylesspalace/tusky/pager/AvatarImagePagerAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/pager/SingleImagePagerAdapter.kt similarity index 81% rename from app/src/main/java/com/keylesspalace/tusky/pager/AvatarImagePagerAdapter.kt rename to app/src/main/java/com/keylesspalace/tusky/pager/SingleImagePagerAdapter.kt index a3dfcf2a..c8306f70 100644 --- a/app/src/main/java/com/keylesspalace/tusky/pager/AvatarImagePagerAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/pager/SingleImagePagerAdapter.kt @@ -5,14 +5,14 @@ import androidx.fragment.app.FragmentActivity import com.keylesspalace.tusky.ViewMediaAdapter import com.keylesspalace.tusky.fragment.ViewMediaFragment -class AvatarImagePagerAdapter( +class SingleImagePagerAdapter( activity: FragmentActivity, - private val avatarUrl: String + private val imageUrl: String ) : ViewMediaAdapter(activity) { override fun createFragment(position: Int): Fragment { return if (position == 0) { - ViewMediaFragment.newAvatarInstance(avatarUrl) + ViewMediaFragment.newSingleImageInstance(imageUrl) } else { throw IllegalStateException() } diff --git a/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt b/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt index 2cb8a2f4..8aa3eb76 100644 --- a/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt @@ -1,6 +1,5 @@ package com.keylesspalace.tusky.repository -import android.text.Spanned import android.text.SpannedString import androidx.core.text.parseAsHtml import androidx.core.text.toHtml @@ -184,14 +183,10 @@ class TimelineRepositoryImpl( } private fun cleanup() { - Single.fromCallable { + Schedulers.io().scheduleDirect { val olderThan = System.currentTimeMillis() - TimelineRepository.CLEANUP_INTERVAL - for (account in accountManager.getAllAccountsOrderedByActive()) { - timelineDao.cleanup(account.id, account.accountId, olderThan) - } + timelineDao.cleanup(olderThan) } - .subscribeOn(Schedulers.io()) - .subscribe() } private fun TimelineStatusWithAccount.toStatus(): TimelineStatus { diff --git a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt index 0a5d3494..df9627d8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt +++ b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt @@ -17,7 +17,7 @@ object PrefKeys { // each preference a key for it to work. const val APP_THEME = "appTheme" - const val EMOJI = "emojiCompat" + const val EMOJI = "selected_emoji_font" const val FAB_HIDE = "fabHide" const val LANGUAGE = "language" const val STATUS_TEXT_SIZE = "statusTextSize" diff --git a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt index 438580ee..82dfa14e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt +++ b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt @@ -3,7 +3,8 @@ package com.keylesspalace.tusky.settings import android.content.Context import androidx.annotation.StringRes import androidx.preference.* -import com.keylesspalace.tusky.EmojiPreference +import com.keylesspalace.tusky.components.preference.EmojiPreference +import okhttp3.OkHttpClient class PreferenceParent( val context: Context, @@ -24,8 +25,8 @@ inline fun PreferenceParent.listPreference(builder: ListPreference.() -> Unit): return pref } -inline fun PreferenceParent.emojiPreference(builder: EmojiPreference.() -> Unit): EmojiPreference { - val pref = EmojiPreference(context) +inline fun PreferenceParent.emojiPreference(okHttpClient: OkHttpClient, builder: EmojiPreference.() -> Unit): EmojiPreference { + val pref = EmojiPreference(context, okHttpClient) builder(pref) addPref(pref) return pref diff --git a/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt index 14a0ceb5..679f38d3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt @@ -54,7 +54,7 @@ fun CharSequence.emojify(emojis: List?, view: View) : CharSequence { while(matcher.find()) { val span = EmojiSpan(WeakReference(view)) - builder.setSpan(span, matcher.start(), matcher.end(), 0); + builder.setSpan(span, matcher.start(), matcher.end(), 0) Glide.with(view) .asBitmap() .load(url) @@ -89,7 +89,7 @@ class EmojiSpan(val viewWeakReference: WeakReference) : ReplacementSpan() drawable.setBounds(0, 0, emojiSize, emojiSize) var transY = bottom - drawable.bounds.bottom - transY -= paint.fontMetricsInt.descent / 2; + transY -= paint.fontMetricsInt.descent / 2 canvas.translate(x, transY.toFloat()) drawable.draw(canvas) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.java b/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.java deleted file mode 100644 index ca7899e7..00000000 --- a/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.java +++ /dev/null @@ -1,564 +0,0 @@ -package com.keylesspalace.tusky.util; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.os.AsyncTask; -import android.util.Log; -import android.util.Pair; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; - -import com.keylesspalace.tusky.R; - -import java.io.EOFException; -import java.io.File; -import java.io.FilenameFilter; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import de.c1710.filemojicompat.FileEmojiCompatConfig; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.ResponseBody; -import okio.BufferedSink; -import okio.Okio; -import okio.Source; - - -/** - * This class bundles information about an emoji font as well as many convenient actions. - */ -public class EmojiCompatFont { - private static final String TAG = "EmojiCompatFont"; - /** - * This String represents the sub-directory the fonts are stored in. - */ - private static final String DIRECTORY = "emoji"; - - // These are the items which are also present in the JSON files - private final String name, display, url; - // The thumbnail image and the caption are provided as resource ids - private final int img, caption; - // The version is stored as a String in the x.xx.xx format (to be able to compare versions) - private final String version; - private final int[] versionCode; - private AsyncTask fontDownloader; - // The system font gets some special behavior... - private static final EmojiCompatFont SYSTEM_DEFAULT = - new EmojiCompatFont("system-default", - "System Default", - R.string.caption_systememoji, - R.drawable.ic_emoji_34dp, - "", - "0"); - private static final EmojiCompatFont BLOBMOJI = - new EmojiCompatFont("Blobmoji", - "Blobmoji", - R.string.caption_blobmoji, - R.drawable.ic_blobmoji, - "https://tusky.app/hosted/emoji/BlobmojiCompat.ttf", - "12.0.0" - ); - private static final EmojiCompatFont TWEMOJI = - new EmojiCompatFont("Twemoji", - "Twemoji", - R.string.caption_twemoji, - R.drawable.ic_twemoji, - "https://tusky.app/hosted/emoji/TwemojiCompat.ttf", - "12.0.0" - ); - private static final EmojiCompatFont NOTOEMOJI = - new EmojiCompatFont("NotoEmoji", - "Noto Emoji", - R.string.caption_notoemoji, - R.drawable.ic_notoemoji, - "https://tusky.app/hosted/emoji/NotoEmojiCompat.ttf", - "11.0.0" - ); - - /** - * This array stores all available EmojiCompat fonts. - * References to them can simply be saved by saving their indices - */ - public static final EmojiCompatFont[] FONTS = {SYSTEM_DEFAULT, BLOBMOJI, TWEMOJI, NOTOEMOJI}; - // A list of all available font files and whether they are older than the current version or not - // They are ordered by there version codes in ascending order - private ArrayList> existingFontFiles; - - private EmojiCompatFont(String name, - String display, - int caption, - int img, - String url, - String version) { - this.name = name; - this.display = display; - this.caption = caption; - this.img = img; - this.url = url; - this.version = version; - this.versionCode = getVersionCode(version); - } - - /** - * Returns the Emoji font associated with this ID - * - * @param id the ID of this font - * @return the corresponding font. Will default to SYSTEM_DEFAULT if not in range. - */ - public static EmojiCompatFont byId(int id) { - if (id >= 0 && id < FONTS.length) { - return FONTS[id]; - } else { - return SYSTEM_DEFAULT; - } - } - - public int getId() { - return Arrays.asList(FONTS).indexOf(this); - } - - public String getName() { - return name; - } - - - public String getDisplay(Context context) { - return this != SYSTEM_DEFAULT ? display : context.getString(R.string.system_default); - } - - public String getCaption(Context context) { - return context.getResources().getString(caption); - } - - public String getUrl() { - return url; - } - - public Drawable getThumb(Context context) { - return ContextCompat.getDrawable(context, img); - } - - public String getVersion() { - return version; - } - - public int[] getVersionCode() { - return versionCode; - } - - /** - * This method will return the actual font file (regardless of its existence) for - * the current version (not necessarily the latest!). - * - * @return The font (TTF) file or null if called on SYSTEM_FONT - */ - @Nullable - private File getFont(Context context) { - if (this != SYSTEM_DEFAULT) { - File directory = new File(context.getExternalFilesDir(null), DIRECTORY); - return new File(directory, this.getName() + this.getVersion() + ".ttf"); - } else { - return null; - } - } - - - public FileEmojiCompatConfig getConfig(Context context) { - return new FileEmojiCompatConfig(context, getLatestFontFile(context)); - } - - public boolean isDownloaded(Context context) { - // The existence of the current version is actually checked twice, although the first method should - // be much faster and more common. - return this == SYSTEM_DEFAULT || getFont(context) != null - && (getFont(context).exists() || newerFileExists(context)); - } - - /** - * Checks whether there is already a font version that satisfies the current version, i.e. it - * has a higher or equal version code. - * - * @param context The Context - * @return Whether there is a font file with a higher or equal version code to the current - */ - private boolean newerFileExists(Context context) { - loadExistingFontFiles(context); - if (!existingFontFiles.isEmpty()) - // The last file is already the newest one... - return compareVersions(existingFontFiles.get(existingFontFiles.size() - 1).second, - getVersionCode()) >= 0; - return false; - } - - /** - * Downloads the TTF file for this font - * - * @param listeners The listeners which will be notified when the download has been finished - */ - public void downloadFont(Context context, Downloader.EmojiDownloadListener... listeners) { - if (this != SYSTEM_DEFAULT) { - // Additionally run a cleanup process after the download has been successful. - Downloader.EmojiDownloadListener cleanup = font -> deleteOldVersions(context); - - List allListeners - = new ArrayList<>(Arrays.asList(listeners)); - allListeners.add(cleanup); - Downloader.EmojiDownloadListener[] allListenersA = - new Downloader.EmojiDownloadListener[allListeners.size()]; - - fontDownloader = new Downloader( - this, - allListeners.toArray(allListenersA)) - .execute(getFont(context)); - } else { - for (Downloader.EmojiDownloadListener listener : listeners) { - // The system emoji font is always downloaded... - listener.onDownloaded(this); - } - } - } - - /** - * Deletes any older version of a font - * - * @param context The current Context - */ - private void deleteOldVersions(Context context) { - loadExistingFontFiles(context); - Log.d(TAG, "deleting old versions..."); - - Log.d(TAG, String.format("deleteOldVersions: Found %d other font files", existingFontFiles.size())); - for (Pair fileExists : existingFontFiles) { - if (compareVersions(fileExists.second, getVersionCode()) < 0) { - File file = fileExists.first; - // Uses side effects! - Log.d(TAG, String.format("Deleted %s successfully: %s", file.getAbsolutePath(), - file.delete())); - } - } - } - - private static final Comparator> pairComparator = (o1, o2) -> compareVersions(o1.second, o2.second); - - - /** - * Loads all font files that are inside the files directory into an ArrayList with the information - * on whether they are older than the currently available version or not. - * - * @param context The Context - */ - private void loadExistingFontFiles(Context context) { - // Only load it once - if (this.existingFontFiles == null) { - // If we call this on the system default font, just return nothing... - if (this == SYSTEM_DEFAULT) { - existingFontFiles = new ArrayList<>(0); - } - - File directory = new File(context.getExternalFilesDir(null), DIRECTORY); - // It will search for old versions using a regex that matches the font's name plus - // (if present) a version code. No version code will be regarded as version 0. - Pattern fontRegex = Pattern.compile(getName() + "(\\d+(\\.\\d+)*)?" + "\\.ttf"); - - - FilenameFilter ttfFilter = (dir, name) -> name.endsWith(".ttf"); - File[] existingFontFiles = directory.isDirectory() ? directory.listFiles(ttfFilter) : new File[0]; - Log.d(TAG, String.format("loadExistingFontFiles: %d other font files found", - existingFontFiles.length)); - // This is actually the upper bound - this.existingFontFiles = new ArrayList<>(existingFontFiles.length); - - - for (File file : existingFontFiles) { - Matcher matcher = fontRegex.matcher(file.getName()); - if (matcher.matches()) { - String version = matcher.group(1); - int[] versionCode = getVersionCode(version); - Pair entry = new Pair<>(file, versionCode); - // https://stackoverflow.com/a/51893026 - // Insert it in a sorted way - int index = Collections.binarySearch(this.existingFontFiles, entry, pairComparator); - if (index < 0) { - index = -index - 1; - } - this.existingFontFiles.add(index, entry); - } - } - } - } - - /** - * Returns the current or latest version of this font file (if there is any) - * - * @param context The Context - * @return The file for this font with the current or (if not existent) highest version code or null if there is no file for this font. - */ - private File getLatestFontFile(@NonNull Context context) { - File current = getFont(context); - if (current != null && current.exists()) - return current; - loadExistingFontFiles(context); - try { - return existingFontFiles.get(existingFontFiles.size() - 1).first; - } catch (IndexOutOfBoundsException e) { - return getFont(context); - } - } - - private @Nullable - int[] getVersionCode(@Nullable String version) { - if (version == null) - return null; - String[] versions = version.split("\\."); - int[] versionCodes = new int[versions.length]; - for (int i = 0; i < versions.length; i++) - versionCodes[i] = parseInt(versions[i], 0); - return versionCodes; - } - - /** - * A small helper method to convert a String to an int with a default value - * - * @param value The String to be parsed - * @param def The default value - * @return Either the String parsed to an int or - if this is not possible - the default value - */ - private int parseInt(@Nullable String value, int def) { - try { - return Integer.parseInt(value); - } catch (NumberFormatException | NullPointerException e) { - e.printStackTrace(); - return def; - } - } - - /** - * Compares two version codes to each other - * - * @param versionA The first version - * @param versionB The second version - * @return -1 if versionA < versionB, 1 if versionA > versionB and 0 otherwise - */ - private static int compareVersions(int[] versionA, int[] versionB) { - // This saves us much headache about handling a null version - if (versionA == null) - versionA = new int[]{0}; - - int len = Math.max(versionB.length, versionA.length); - - int vA, vB; - // Compare the versions - for (int i = 0; i < len; i++) { - // Just to make sure there is something specified here - if (versionA.length > i) { - vA = versionA[i]; - } else { - vA = 0; - } - if (versionB.length > i) { - vB = versionB[i]; - } else { - vB = 0; - } - - // It needs to be decided on the next level - if (vB == vA) - continue; - // Okay, is version B newer or version A? - return Integer.compare(vA, vB); - } - - // The versions are equal - return 0; - } - - /** - * Stops downloading the font. If no one started a font download, nothing happens. - */ - public void cancelDownload() { - if (fontDownloader != null) { - fontDownloader.cancel(false); - fontDownloader = null; - } - } - - /** - * This class is used to easily manage the download of a font - */ - public static class Downloader extends AsyncTask { - // All interested objects/methods - private final EmojiDownloadListener[] listeners; - // The MIME-Type which might be unnecessary - private static final String MIME = "application/woff"; - // The font belonging to this download - private final EmojiCompatFont font; - private static final String TAG = "Emoji-Font Downloader"; - private static long CHUNK_SIZE = 4096; - private boolean failed = false; - - Downloader(EmojiCompatFont font, EmojiDownloadListener... listeners) { - super(); - this.listeners = listeners; - this.font = font; - } - - @Override - protected File doInBackground(File... files) { - // Only download to one file... - File downloadFile = files[0]; - try { - // It is possible (and very likely) that the file does not exist yet - if (!downloadFile.exists()) { - downloadFile.getParentFile().mkdirs(); - downloadFile.createNewFile(); - } - OkHttpClient client = new OkHttpClient(); - Request request = new Request.Builder().url(font.getUrl()) - .addHeader("Content-Type", MIME) - .build(); - Response response = client.newCall(request).execute(); - BufferedSink sink = Okio.buffer(Okio.sink(downloadFile)); - Source source = null; - try { - long size; - // Download! - if (response.body() != null - && response.isSuccessful() - && (size = networkResponseLength(response)) > 0) { - float progress = 0; - source = response.body().source(); - try { - while (!isCancelled()) { - sink.write(response.body().source(), CHUNK_SIZE); - progress += CHUNK_SIZE; - publishProgress(progress / size); - } - } catch (EOFException ex) { - /* - This means we've finished downloading the file since sink.write - will throw an EOFException when the file to be read is empty. - */ - } - } else { - Log.e(TAG, "downloading " + font.getUrl() + " failed. No content to download."); - Log.e(TAG, "Status code: " + response.code()); - failed = true; - } - } finally { - if (source != null) { - source.close(); - } - sink.close(); - // This 'if' uses side effects to delete the File. - if (isCancelled() && !downloadFile.delete()) { - Log.e(TAG, "Could not delete file " + downloadFile); - } - } - } catch (IOException ex) { - ex.printStackTrace(); - failed = true; - } - return downloadFile; - } - - @Override - public void onProgressUpdate(Float... progress) { - for (EmojiDownloadListener listener : listeners) { - listener.onProgress(progress[0]); - } - } - - @Override - public void onPostExecute(File downloadedFile) { - if (!failed && downloadedFile.exists()) { - for (EmojiDownloadListener listener : listeners) { - listener.onDownloaded(font); - } - } else { - fail(downloadedFile); - } - } - - private void fail(File failedFile) { - if (failedFile.exists() && !failedFile.delete()) { - Log.e(TAG, "Could not delete file " + failedFile); - } - for (EmojiDownloadListener listener : listeners) { - listener.onFailed(); - } - } - - /** - * This interfaced is used to get notified when a download has been finished - */ - public interface EmojiDownloadListener { - /** - * Called after successfully finishing a download. - * - * @param font The font related to this download. This will help identifying the download - */ - void onDownloaded(EmojiCompatFont font); - - // TODO: Add functionality - - /** - * Called when something went wrong with the download. - * This one won't be called when the download has been cancelled though. - */ - default void onFailed() { - // Oh no! D: - } - - /** - * Called whenever the progress changed - * - * @param Progress A value between 0 and 1 representing the current progress - */ - default void onProgress(float Progress) { - // ARE WE THERE YET? - } - } - - - /** - * This method is needed because when transparent compression is used OkHttp reports - * {@link ResponseBody#contentLength()} as -1. We try to get the header which server sent - * us manually here. - * - * @see OkHttp issue 259 - */ - private long networkResponseLength(Response response) { - Response networkResponse = response.networkResponse(); - if (networkResponse == null) { - // In case it's a fully cached response - ResponseBody body = response.body(); - return body == null ? -1 : body.contentLength(); - } - String header = networkResponse.header("Content-Length"); - if (header == null) { - return -1; - } - try { - return Integer.parseInt(header); - } catch (NumberFormatException e) { - return -1; - } - } - } - - @Override - @NonNull - public String toString() { - return display; - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt b/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt new file mode 100644 index 00000000..4697a1e9 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt @@ -0,0 +1,351 @@ +package com.keylesspalace.tusky.util + +import android.content.Context +import android.util.Log +import android.util.Pair +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.annotation.VisibleForTesting +import com.keylesspalace.tusky.R +import de.c1710.filemojicompat.FileEmojiCompatConfig +import io.reactivex.Observable +import io.reactivex.ObservableEmitter +import io.reactivex.schedulers.Schedulers +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import okhttp3.ResponseBody +import okhttp3.internal.toLongOrDefault +import okio.Source +import okio.buffer +import okio.sink +import java.io.EOFException +import java.io.File +import java.io.FilenameFilter +import java.io.IOException +import kotlin.math.max + +/** + * This class bundles information about an emoji font as well as many convenient actions. + */ +class EmojiCompatFont( + val name: String, + private val display: String, + @StringRes val caption: Int, + @DrawableRes val img: Int, + val url: String, + // The version is stored as a String in the x.xx.xx format (to be able to compare versions) + val version: String) { + + private val versionCode = getVersionCode(version) + + // A list of all available font files and whether they are older than the current version or not + // They are ordered by their version codes in ascending order + private var existingFontFileCache: List>>? = null + + val id: Int + get() = FONTS.indexOf(this) + + fun getDisplay(context: Context): String { + return if (this !== SYSTEM_DEFAULT) display else context.getString(R.string.system_default) + } + + /** + * This method will return the actual font file (regardless of its existence) for + * the current version (not necessarily the latest!). + * + * @return The font (TTF) file or null if called on SYSTEM_FONT + */ + private fun getFontFile(context: Context): File? { + return if (this !== SYSTEM_DEFAULT) { + val directory = File(context.getExternalFilesDir(null), DIRECTORY) + File(directory, "$name$version.ttf") + } else { + null + } + } + + fun getConfig(context: Context): FileEmojiCompatConfig { + return FileEmojiCompatConfig(context, getLatestFontFile(context)) + } + + fun isDownloaded(context: Context): Boolean { + return this === SYSTEM_DEFAULT || getFontFile(context)?.exists() == true || fontFileExists(context) + } + + /** + * Checks whether there is already a font version that satisfies the current version, i.e. it + * has a higher or equal version code. + * + * @param context The Context + * @return Whether there is a font file with a higher or equal version code to the current + */ + private fun fontFileExists(context: Context): Boolean { + val existingFontFiles = getExistingFontFiles(context) + return if (existingFontFiles.isNotEmpty()) { + compareVersions(existingFontFiles.last().second, versionCode) >= 0 + } else { + false + } + } + + /** + * Deletes any older version of a font + * + * @param context The current Context + */ + private fun deleteOldVersions(context: Context) { + val existingFontFiles = getExistingFontFiles(context) + Log.d(TAG, "deleting old versions...") + Log.d(TAG, String.format("deleteOldVersions: Found %d other font files", existingFontFiles.size)) + for (fileExists in existingFontFiles) { + if (compareVersions(fileExists.second, versionCode) < 0) { + val file = fileExists.first + // Uses side effects! + Log.d(TAG, String.format("Deleted %s successfully: %s", file.absolutePath, + file.delete())) + } + } + } + + /** + * Loads all font files that are inside the files directory into an ArrayList with the information + * on whether they are older than the currently available version or not. + * + * @param context The Context + */ + private fun getExistingFontFiles(context: Context): List>> { + // Only load it once + existingFontFileCache?.let { + return it + } + // If we call this on the system default font, just return nothing... + if (this === SYSTEM_DEFAULT) { + existingFontFileCache = emptyList() + return emptyList() + } + + val directory = File(context.getExternalFilesDir(null), DIRECTORY) + // It will search for old versions using a regex that matches the font's name plus + // (if present) a version code. No version code will be regarded as version 0. + val fontRegex = "$name(\\d+(\\.\\d+)*)?\\.ttf".toPattern() + val ttfFilter = FilenameFilter { _, name: String -> name.endsWith(".ttf") } + val foundFontFiles = directory.listFiles(ttfFilter).orEmpty() + Log.d(TAG, String.format("loadExistingFontFiles: %d other font files found", + foundFontFiles.size)) + + return foundFontFiles.map { file -> + val matcher = fontRegex.matcher(file.name) + val versionCode = if (matcher.matches()) { + val version = matcher.group(1) + getVersionCode(version) + } else { + listOf(0) + } + Pair(file, versionCode) + }.sortedWith { a, b -> + compareVersions(a.second, b.second) + }.also { + existingFontFileCache = it + } + } + + /** + * Returns the current or latest version of this font file (if there is any) + * + * @param context The Context + * @return The file for this font with the current or (if not existent) highest version code or null if there is no file for this font. + */ + private fun getLatestFontFile(context: Context): File? { + val current = getFontFile(context) + if (current != null && current.exists()) return current + val existingFontFiles = getExistingFontFiles(context) + return existingFontFiles.firstOrNull()?.first + } + + private fun getVersionCode(version: String?): List { + if (version == null) return listOf(0) + return version.split(".").map { + it.toIntOrNull() ?: 0 + } + } + + fun downloadFontFile(context: Context, + okHttpClient: OkHttpClient): Observable { + return Observable.create { emitter: ObservableEmitter -> + // It is possible (and very likely) that the file does not exist yet + val downloadFile = getFontFile(context)!! + if (!downloadFile.exists()) { + downloadFile.parentFile?.mkdirs() + downloadFile.createNewFile() + } + val request = Request.Builder().url(url) + .build() + + val sink = downloadFile.sink().buffer() + var source: Source? = null + try { + // Download! + val response = okHttpClient.newCall(request).execute() + + val responseBody = response.body + if (response.isSuccessful && responseBody != null) { + val size = response.length() + var progress = 0f + source = responseBody.source() + try { + while (!emitter.isDisposed) { + sink.write(source, CHUNK_SIZE) + progress += CHUNK_SIZE.toFloat() + if(size > 0) { + emitter.onNext(progress / size) + } else { + emitter.onNext(-1f) + } + } + } catch (ex: EOFException) { + /* + This means we've finished downloading the file since sink.write + will throw an EOFException when the file to be read is empty. + */ + } + } else { + Log.e(TAG, "Downloading $url failed. Status code: ${response.code}") + emitter.tryOnError(Exception()) + } + + } catch (ex: IOException) { + Log.e(TAG, "Downloading $url failed.", ex) + downloadFile.deleteIfExists() + emitter.tryOnError(ex) + } finally { + source?.close() + sink.close() + if (emitter.isDisposed) { + downloadFile.deleteIfExists() + } else { + deleteOldVersions(context) + emitter.onComplete() + } + } + + } + .subscribeOn(Schedulers.io()) + + } + + /** + * Deletes the downloaded file, if it exists. Should be called when a download gets cancelled. + */ + fun deleteDownloadedFile(context: Context) { + getFontFile(context)?.deleteIfExists() + } + + override fun toString(): String { + return display + } + + companion object { + private const val TAG = "EmojiCompatFont" + + /** + * This String represents the sub-directory the fonts are stored in. + */ + private const val DIRECTORY = "emoji" + + private const val CHUNK_SIZE = 4096L + + // The system font gets some special behavior... + private val SYSTEM_DEFAULT = EmojiCompatFont("system-default", + "System Default", + R.string.caption_systememoji, + R.drawable.ic_emoji_34dp, + "", + "0") + private val BLOBMOJI = EmojiCompatFont("Blobmoji", + "Blobmoji", + R.string.caption_blobmoji, + R.drawable.ic_blobmoji, + "https://tusky.app/hosted/emoji/BlobmojiCompat.ttf", + "12.0.0" + ) + private val TWEMOJI = EmojiCompatFont("Twemoji", + "Twemoji", + R.string.caption_twemoji, + R.drawable.ic_twemoji, + "https://tusky.app/hosted/emoji/TwemojiCompat.ttf", + "12.0.0" + ) + private val NOTOEMOJI = EmojiCompatFont("NotoEmoji", + "Noto Emoji", + R.string.caption_notoemoji, + R.drawable.ic_notoemoji, + "https://tusky.app/hosted/emoji/NotoEmojiCompat.ttf", + "11.0.0" + ) + + /** + * This array stores all available EmojiCompat fonts. + * References to them can simply be saved by saving their indices + */ + val FONTS = listOf(SYSTEM_DEFAULT, BLOBMOJI, TWEMOJI, NOTOEMOJI) + + /** + * Returns the Emoji font associated with this ID + * + * @param id the ID of this font + * @return the corresponding font. Will default to SYSTEM_DEFAULT if not in range. + */ + fun byId(id: Int): EmojiCompatFont = FONTS.getOrElse(id) { SYSTEM_DEFAULT } + + /** + * Compares two version codes to each other + * + * @param versionA The first version + * @param versionB The second version + * @return -1 if versionA < versionB, 1 if versionA > versionB and 0 otherwise + */ + @VisibleForTesting + fun compareVersions(versionA: List, versionB: List): Int { + val len = max(versionB.size, versionA.size) + for (i in 0 until len) { + + val vA = versionA.getOrElse(i) { 0 } + val vB = versionB.getOrElse(i) { 0 } + + // It needs to be decided on the next level + if (vA == vB) continue + // Okay, is version B newer or version A? + return vA.compareTo(vB) + } + + // The versions are equal + return 0 + } + + /** + * This method is needed because when transparent compression is used OkHttp reports + * [ResponseBody.contentLength] as -1. We try to get the header which server sent + * us manually here. + * + * @see [OkHttp issue 259](https://github.com/square/okhttp/issues/259) + */ + private fun Response.length(): Long { + networkResponse?.let { + val header = it.header("Content-Length") ?: return -1 + return header.toLongOrDefault(-1) + } + + // In case it's a fully cached response + return body?.contentLength() ?: -1 + } + + private fun File.deleteIfExists() { + if(exists() && !delete()) { + Log.e(TAG, "Could not delete file $this") + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt index e747c3e3..cb4651fd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt @@ -285,8 +285,7 @@ class StatusViewHelper(private val itemView: View) { if (useAbsoluteTime) { context.getString(R.string.poll_info_time_absolute, getAbsoluteTime(poll.expiresAt)) } else { - val pollDuration = TimestampUtils.formatPollDuration(context, poll.expiresAt!!.time, timestamp) - context.getString(R.string.poll_info_time_relative, pollDuration) + TimestampUtils.formatPollDuration(context, poll.expiresAt!!.time, timestamp) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StringUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/StringUtils.kt index c7af0ca6..83eaeafa 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/StringUtils.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/StringUtils.kt @@ -80,3 +80,12 @@ fun Spanned.trimTrailingWhitespace(): Spanned { } while (i >= 0 && get(i).isWhitespace()) return subSequence(0, i + 1) as Spanned } + +/** + * BidiFormatter.unicodeWrap is insufficient in some cases (see #1921) + * So we force isolation manually + * https://unicode.org/reports/tr9/#Explicit_Directional_Isolates + */ +fun CharSequence.unicodeWrap(): String { + return "\u2068${this}\u2069" +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/view/EmojiPicker.kt b/app/src/main/java/com/keylesspalace/tusky/view/EmojiPicker.kt new file mode 100644 index 00000000..09e648ad --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/EmojiPicker.kt @@ -0,0 +1,17 @@ +package com.keylesspalace.tusky.view + +import android.content.Context +import android.util.AttributeSet +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView + +class EmojiPicker @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : RecyclerView(context, attrs) { + + init { + clipToPadding = false + layoutManager = GridLayoutManager(context, 3, GridLayoutManager.HORIZONTAL, false) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt b/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt index 2702a333..44a70267 100644 --- a/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt @@ -17,7 +17,7 @@ fun showMuteAccountDialog( (view.findViewById(R.id.warning) as TextView).text = activity.getString(R.string.dialog_mute_warning, accountUsername) val checkbox: CheckBox = view.findViewById(R.id.checkbox) - checkbox.setChecked(true) + checkbox.isChecked = true AlertDialog.Builder(activity) .setView(view) diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt index bf887974..593f43d3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt @@ -2,7 +2,6 @@ package com.keylesspalace.tusky.viewmodel import android.util.Log import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import com.keylesspalace.tusky.appstore.* import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.entity.Account @@ -11,70 +10,66 @@ import com.keylesspalace.tusky.entity.IdentityProof import com.keylesspalace.tusky.entity.Relationship import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.* +import io.reactivex.Single import io.reactivex.disposables.Disposable import retrofit2.Call import retrofit2.Callback import retrofit2.Response +import java.util.concurrent.TimeUnit import javax.inject.Inject class AccountViewModel @Inject constructor( private val mastodonApi: MastodonApi, private val eventHub: EventHub, private val accountManager: AccountManager -) : ViewModel() { +) : RxAwareViewModel() { val accountData = MutableLiveData>() val relationshipData = MutableLiveData>() + val noteSaved = MutableLiveData() + private val identityProofData = MutableLiveData>() val accountFieldData = combineOptionalLiveData(accountData, identityProofData) { accountRes, identityProofs -> identityProofs.orEmpty().map { Either.Left(it) } - .plus(accountRes?.data?.fields.orEmpty().map { Either.Right(it) }) + .plus(accountRes?.data?.fields.orEmpty().map { Either.Right(it) }) } - - private val callList: MutableList> = mutableListOf() - private val disposable: Disposable = eventHub.events - .subscribe { event -> - if (event is ProfileEditedEvent && event.newProfileData.id == accountData.value?.data?.id) { - accountData.postValue(Success(event.newProfileData)) - } - } - val isRefreshing = MutableLiveData().apply { value = false } private var isDataLoading = false lateinit var accountId: String var isSelf = false + private var noteDisposable: Disposable? = null + + init { + eventHub.events + .subscribe { event -> + if (event is ProfileEditedEvent && event.newProfileData.id == accountData.value?.data?.id) { + accountData.postValue(Success(event.newProfileData)) + } + }.autoDispose() + } + private fun obtainAccount(reload: Boolean = false) { if (accountData.value == null || reload) { isDataLoading = true accountData.postValue(Loading()) - val call = mastodonApi.account(accountId) - call.enqueue(object : Callback { - override fun onResponse(call: Call, - response: Response) { - if (response.isSuccessful) { - accountData.postValue(Success(response.body())) - } else { + mastodonApi.account(accountId) + .subscribe({ account -> + accountData.postValue(Success(account)) + isDataLoading = false + isRefreshing.postValue(false) + }, {t -> + Log.w(TAG, "failed obtaining account", t) accountData.postValue(Error()) - } - isDataLoading = false - isRefreshing.postValue(false) - } - - override fun onFailure(call: Call, t: Throwable) { - Log.w(TAG, "failed obtaining account", t) - accountData.postValue(Error()) - isDataLoading = false - isRefreshing.postValue(false) - } - }) - - callList.add(call) + isDataLoading = false + isRefreshing.postValue(false) + }) + .autoDispose() } } @@ -83,51 +78,27 @@ class AccountViewModel @Inject constructor( relationshipData.postValue(Loading()) - val ids = listOf(accountId) - val call = mastodonApi.relationships(ids) - call.enqueue(object : Callback> { - override fun onResponse(call: Call>, - response: Response>) { - val relationships = response.body() - if (response.isSuccessful && relationships != null && relationships.getOrNull(0) != null) { - val relationship = relationships[0] - relationshipData.postValue(Success(relationship)) - } else { + mastodonApi.relationships(listOf(accountId)) + .subscribe({ relationships -> + relationshipData.postValue(Success(relationships[0])) + }, { t -> + Log.w(TAG, "failed obtaining relationships", t) relationshipData.postValue(Error()) - } - } - - override fun onFailure(call: Call>, t: Throwable) { - Log.w(TAG, "failed obtaining relationships", t) - relationshipData.postValue(Error()) - } - }) - - callList.add(call) + }) + .autoDispose() } } private fun obtainIdentityProof(reload: Boolean = false) { if (identityProofData.value == null || reload) { - val call = mastodonApi.identityProofs(accountId) - call.enqueue(object : Callback> { - override fun onResponse(call: Call>, - response: Response>) { - val proofs = response.body() - if (response.isSuccessful && proofs != null ) { + mastodonApi.identityProofs(accountId) + .subscribe({ proofs -> identityProofData.postValue(proofs) - } else { - identityProofData.postValue(emptyList()) - } - } - - override fun onFailure(call: Call>, t: Throwable) { - Log.w(TAG, "failed obtaining identity proofs", t) - } - }) - - callList.add(call) + }, { t -> + Log.w(TAG, "failed obtaining identity proofs", t) + }) + .autoDispose() } } @@ -230,11 +201,15 @@ class AccountViewModel @Inject constructor( relationshipData.postValue(Loading(newRelation)) } - val callback = object : Callback { - override fun onResponse(call: Call, - response: Response) { - val relationship = response.body() - if (response.isSuccessful && relationship != null) { + when (relationshipAction) { + RelationShipAction.FOLLOW -> mastodonApi.followAccount(accountId, parameter ?: true) + RelationShipAction.UNFOLLOW -> mastodonApi.unfollowAccount(accountId) + RelationShipAction.BLOCK -> mastodonApi.blockAccount(accountId) + RelationShipAction.UNBLOCK -> mastodonApi.unblockAccount(accountId) + RelationShipAction.MUTE -> mastodonApi.muteAccount(accountId, parameter ?: true) + RelationShipAction.UNMUTE -> mastodonApi.unmuteAccount(accountId) + }.subscribe( + { relationship -> relationshipData.postValue(Success(relationship)) when (relationshipAction) { @@ -244,37 +219,35 @@ class AccountViewModel @Inject constructor( else -> { } } - - } else { + }, + { relationshipData.postValue(Error(relation)) } + ) + .autoDispose() + } - } - - override fun onFailure(call: Call, t: Throwable) { - relationshipData.postValue(Error(relation)) - } - } - - val call = when (relationshipAction) { - RelationShipAction.FOLLOW -> mastodonApi.followAccount(accountId, parameter ?: true) - RelationShipAction.UNFOLLOW -> mastodonApi.unfollowAccount(accountId) - RelationShipAction.BLOCK -> mastodonApi.blockAccount(accountId) - RelationShipAction.UNBLOCK -> mastodonApi.unblockAccount(accountId) - RelationShipAction.MUTE -> mastodonApi.muteAccount(accountId, parameter ?: true) - RelationShipAction.UNMUTE -> mastodonApi.unmuteAccount(accountId) - } - - call.enqueue(callback) - callList.add(call) - + fun noteChanged(newNote: String) { + noteSaved.postValue(false) + noteDisposable?.dispose() + noteDisposable = Single.timer(1500, TimeUnit.MILLISECONDS) + .flatMap { + mastodonApi.updateAccountNote(accountId, newNote) + } + .doOnSuccess { + noteSaved.postValue(true) + } + .delay(4, TimeUnit.SECONDS) + .subscribe({ + noteSaved.postValue(false) + }, { + Log.e(TAG, "Error updating note", it) + }) } override fun onCleared() { - callList.forEach { - it.cancel() - } - disposable.dispose() + super.onCleared() + noteDisposable?.dispose() } fun refresh() { diff --git a/app/src/main/res/drawable/ic_bullhorn_24dp.xml b/app/src/main/res/drawable/ic_bullhorn_24dp.xml new file mode 100644 index 00000000..e290b24e --- /dev/null +++ b/app/src/main/res/drawable/ic_bullhorn_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_account.xml b/app/src/main/res/layout/activity_account.xml index 9a36d1a7..fe9f9a73 100644 --- a/app/src/main/res/layout/activity_account.xml +++ b/app/src/main/res/layout/activity_account.xml @@ -168,16 +168,43 @@ app:barrierDirection="bottom" app:constraint_referenced_ids="accountFollowsYouTextView,accountBadgeTextView" /> + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_compose.xml b/app/src/main/res/layout/activity_compose.xml index abff9502..5f8d6ffd 100644 --- a/app/src/main/res/layout/activity_compose.xml +++ b/app/src/main/res/layout/activity_compose.xml @@ -193,14 +193,12 @@ android:textSize="?attr/status_text_medium" /> - + tools:context="com.keylesspalace.tusky.components.preference.PreferencesActivity"> diff --git a/app/src/main/res/layout/activity_tab_preference.xml b/app/src/main/res/layout/activity_tab_preference.xml index 9bc8d6d1..476b7cbd 100644 --- a/app/src/main/res/layout/activity_tab_preference.xml +++ b/app/src/main/res/layout/activity_tab_preference.xml @@ -2,6 +2,7 @@ @@ -19,8 +20,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:background="?attr/scrimBackground" - android:visibility="invisible" - app:layout_behavior="@string/fab_transformation_scrim_behavior" /> + android:visibility="invisible" /> - + app:cardElevation="2dp"> - + + \ No newline at end of file diff --git a/app/src/main/res/layout/card_license.xml b/app/src/main/res/layout/card_license.xml index 1b1d2647..d5b6cdaa 100644 --- a/app/src/main/res/layout/card_license.xml +++ b/app/src/main/res/layout/card_license.xml @@ -1,30 +1,35 @@ + tools:parentTag="com.google.android.material.card.MaterialCardView"> + android:orientation="vertical" + android:padding="8dp"> + + android:fontFamily="sans-serif-medium" + android:textColor="?android:attr/textColorSecondary" /> + + android:layout_height="wrap_content" + android:textColor="?android:attr/textColorTertiary" /> + + android:textColor="@color/tusky_blue" /> \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_emojicompat.xml b/app/src/main/res/layout/dialog_emojicompat.xml index 923b69ea..6850e045 100644 --- a/app/src/main/res/layout/dialog_emojicompat.xml +++ b/app/src/main/res/layout/dialog_emojicompat.xml @@ -1,33 +1,25 @@ - - + - + - + - - - - + + android:textColor="?android:attr/textColorSecondary" /> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/item_announcement.xml b/app/src/main/res/layout/item_announcement.xml new file mode 100644 index 00000000..185825f1 --- /dev/null +++ b/app/src/main/res/layout/item_announcement.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_follow_request_notification.xml b/app/src/main/res/layout/item_follow_request_notification.xml index 712c08d4..d4db2a3d 100644 --- a/app/src/main/res/layout/item_follow_request_notification.xml +++ b/app/src/main/res/layout/item_follow_request_notification.xml @@ -20,7 +20,7 @@ android:gravity="center_vertical" android:maxLines="1" android:paddingStart="28dp" - android:textColor="?android:textColorTertiary" + android:textColor="?android:textColorSecondary" android:textSize="?attr/status_text_medium" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" diff --git a/app/src/main/res/layout/item_status.xml b/app/src/main/res/layout/item_status.xml index cd6ce980..8d831757 100644 --- a/app/src/main/res/layout/item_status.xml +++ b/app/src/main/res/layout/item_status.xml @@ -476,6 +476,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="6dp" + android:textSize="?attr/status_text_medium" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="@id/status_display_name" app:layout_constraintTop_toBottomOf="@id/status_poll_button" diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 19b064e2..52d36870 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -349,7 +349,6 @@ تحرير تبويق كتابة هل تريد حقا مسح كافة إشعاراتك؟ - %s متبقي ينتهي في %s انتهى صَوِّت @@ -384,36 +383,36 @@ إظهار علامة البوتات - %d أيام - %d يوم - %d يومين - %d أيام - %d أيام - %d أيام + %d أيام متبقية + %d يوم متبقي + %d يومين متبقيين + %d أيام متبقية + %d أيام متبقية + %d أيام متبقية - %d ساعات - %d ساعة - %d ساعتين - %d ساعات - %d ساعات - %d ساعات + %d ساعات متبقية + %d ساعة متبقية + %d ساعتان متبقيتان + %d ساعات متبقية + %d ساعات متبقية + %d ساعات متبقية - %d دقائق - %d دقيقة - %d دقيقتين - %d دقائق - %d دقائق - %d دقائق + %d دقائق متبقية + %d دقيقة متبقية + %d دقيقتان متبقيتان + %d دقائق متبقية + %d دقائق متبقية + %d دقائق متبقية - %d ثوان - %d ثانية - %d ثانيتين - %d ثوان - %d ثوان - %d ثوان + %d ثوان متبقية + %d ثانية متبقية + %d ثانيتان متبقيتان + %d ثوان متبقية + %d ثوان متبقية + %d ثوان متبقية إجراءات على الصورة %s حزمة الإيموجي الحالية لـ غوغل diff --git a/app/src/main/res/values-ber/strings.xml b/app/src/main/res/values-ber/strings.xml index b2992ab3..2e0412ca 100644 --- a/app/src/main/res/values-ber/strings.xml +++ b/app/src/main/res/values-ber/strings.xml @@ -10,7 +10,7 @@ ⵎⴷⴻⵍ ⵝⴻⵍⵍⴰ ⴷ ⵝⵓⵛⴹⴰ. ⵝⴰⴲⴸⴰⵔⵉⵏ - ⵝⴰⴲⴸⴰⵔⵉⵏ + ⵝⵉⴲⴸⴰⵔⵉⵏ ⵖⴻⴼ ⵡⴻⵏⵏⴻⵣ ⵝⵉⴽⴻⵍⵜ ⵏⵏⵉⴸⴻⵏ ⵏⴰⴸⵉ @@ -22,12 +22,22 @@ ⴽⴻⵎⵎⴻⵍ ⵍⵇⴻⵎ ⴽⴽⴻⵙ - ⵊⴻⵡⵡⴻⵇ + ⵊⴻⵡⵡⴻⵇ! ⵊⴻⵡⵡⴻⵇ ⵇⵇⴻⵏ ⵖⴻⵔ ⵎⴰⵚⵟⵓⴷⵓⵏ - ⴸ ⴰⵛⵓ ⵓ ⵜⵙⵓⵎⵎⴰⵏⵜ\? + ⴸ ⴰⵛⵓ ⵓⴸ ⵜⵜⵓⵎⵎⴰⵏⵜ\? ⵉⵙⵎⴻⵏⵢⵉⴼⴻⵏ ⴽⴽⴻⵙ ⵉⵔⴻⵡⵡⴰⵢⴻⵏ ⵉⵎⵙⴻⵇⴷⴰⵛⴻⵏ ⵜⵙⵡⴰⵃⵍⴻⵎ + ⵉⵛⵛⴰⵔⴻⵏ + ⴰⵏⵜⴰ ⵝⵓⵎⵎⴰⵏⵜ\? + ⵔⵏⵓ ⵢⵉⵡⴻⵏ ⵏ ⵓⵎⵉⴹⴰⵏ ⴰⵎⴰⵢⵏⵓⵝ ⵏ ⵎⴰⵚⵟⵓⴷⵓⵏ + ⵔⵏⵓ ⴰⵎⵉⴹⴰⵏ + ⴸⴰⵛⵓ ⵉⴳⴻⵍⵍⴰⵏ ⴸ ⴰⵎⴰⵢⵏⵓⵝ\? + ⵙⵖⵉⵡⴻⵙ ⵝⵉⵊⴻⵡⵡⵉⵇⵝⴰ + ⵝⵉⵊⴻⵡⵡⵉⵇⵉⵏ ⵢⴻⵜⵜⵖⴰⵙⵖⴰⵡⵙⴻⵏ + ⵝⵉⵊⴻⵡⵡⵉⵇⵉⵏ ⵢⴻⵜⵜⵖⴰⵙⵖⴰⵡⵙⴻⵏ + ⵝⵉⵛⵔⴰⴹ + ⵝⵉⵛⵔⴰⴹ \ No newline at end of file diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml index 42b80c1e..e14d254e 100644 --- a/app/src/main/res/values-bn-rBD/strings.xml +++ b/app/src/main/res/values-bn-rBD/strings.xml @@ -1,7 +1,7 @@ %1$s - %ds + %dসে পছন্দগুলি সহ নর্বাচন: %1$s, %2$s, %3$s, %4$s; %5$s সরাসরি অনুগামিবৃন্দ diff --git a/app/src/main/res/values-bn-rIN/strings.xml b/app/src/main/res/values-bn-rIN/strings.xml index 118321b6..d2e7a0aa 100644 --- a/app/src/main/res/values-bn-rIN/strings.xml +++ b/app/src/main/res/values-bn-rIN/strings.xml @@ -366,7 +366,6 @@ আপনি কি আপনার সমস্ত বিজ্ঞপ্তি স্থায়ীভাবে মুছে ফেলতে চান\? ছবি %s এর জন্য ক্রিয়া %1$s • %2$s - %s বাকি %s এ শেষ হবে বন্ধ ভোট @@ -424,8 +423,8 @@ নিঃশব্দ @%s\? অবরুদ্ধ @%s\? - %d দিন - %d দিন + %d দিন বাকি + %d দিন বাকি লুকানো মিডিয়ার জন্য রঙিন গ্রেডিয়েন্ট ব্যবহার করি আলাপ বন্ধ করো @@ -437,16 +436,16 @@ অনুরোধ অনুসরণ করুন বুস্ট করার আগে নিশ্চিত করো - %d সেকেন্ড - %d সেকেন্ড + %d সেকেন্ড বাকি + %d সেকেন্ড বাকি - %d মিনিট - %d মিনিট + %d মিনিট বাকি + %d মিনিট বাকি - %d ঘন্টা - %d ঘন্টা + %d ঘন্টা বাকি + %d ঘন্টা বাকি %s জন @@ -470,4 +469,5 @@ তোমার কোনো সময়সূচীত স্ট্যাটাস নেই। তোমার কোনো খসড়া নেই। মাস্টোডনের সর্বনিম্ন ৫ মিনিটের সময়সূচীর বিরতি আছে। + শীর্ষস্থানীয় সরঞ্জামদণ্ডের শিরোনামটি লুকান \ No newline at end of file diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index e95715c1..882a3f38 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -330,12 +330,12 @@ Utilitzar el temps absolut La informació de sota pot mostrar el perfil incomplert de l\'usuari. Clica per obrir el perfil complert al navegador. - - + %1$s Favorit + %1$s Favorits - - + + Impulsat per Marcat favorit per @@ -364,27 +364,10 @@ %s vots %s vots - %s restants Acaba a %s tancat L\'enquesta on has votat està tancada La enquesta que heu creat ha finalitzat - - - - - - - - - - - - - - - - Advertència: %s Toot fixat Toot no fixat diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 8a2a2e81..d7a5463e 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -377,7 +377,6 @@ %s hlasy %s hlasů - zbývá %s končí v %s uzavřena Hlasovat @@ -388,24 +387,19 @@ Anketa, ve které jste hlasoval/a, skončila Anketa, kterou jste vytvořil/a, skončila - - - - - - - - + zbývá %d den + zbývá %d dny + zbývá %d dní - %d minuta - %d minut - + zbývá %d minuta + zbývá %d minuty + zbývá %d minut - - - + zbývá %d sekunda + zbývá %d sekundy + zbývá %d sekund Animovat avatary GIF Anketa s volbami: %1$s, %2$s, %3$s, %4$s; %5$s @@ -482,9 +476,9 @@ Odkrýt %s Ztišit @%s\? - %s osoba - %s osoby - %s osob + + + %s požádal/a aby vás mohl/a sledovat Zobrazit dialogové okno s potvrzením při boostování diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 666396ea..edf0e7f6 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1,7 +1,7 @@ Ein Fehler ist aufgetreten. - Ein Netzwerkfehler ist aufgetreten! Bitte überprüfen deine Internetverbindung und versuche es erneut! + Ein Netzwerkfehler ist aufgetreten! Bitte überprüfen Sie Ihre Internetverbindung und versuchen Sie es erneut! Dies darf nicht leer sein. Ungültige Domain angegeben Authentifizieren mit dieser Instanz fehlgeschlagen. @@ -10,7 +10,7 @@ Autorisierung fehlgeschlagen. Es konnte kein Login-Token abgerufen werden. Der Beitrag ist zu lang! - Die Datei muss kleiner als 8MB sein. + Die Datei muss kleiner als 8 MB sein. Videodateien müssen kleiner als 40 MB sein. Dieser Dateityp darf nicht hochgeladen werden. Die Datei konnte nicht geöffnet werden. @@ -18,7 +18,7 @@ Eine Berechtigung wird zum Speichern des Mediums benötigt. Bilder und Videos können nicht beide gleichzeitig an einen Beitrag angehängt werden. Die Mediendatei konnte nicht hochgeladen werden. - Fehler beim Senden des Status + Fehler beim Senden des Beitrags. Start Benachrichtigungen Lokal @@ -36,7 +36,7 @@ Blockierte Profile Folgeanfragen Dein Profil bearbeiten - Gespeicherte Beiträge + Entwürfe Lizenzen \@%s %s teilte @@ -59,7 +59,7 @@ Teilen Boost entfernen Favorisieren - Favoriten entfernen + Favorisierung entfernen Mehr Beitrag erstellen Anmelden mit Mastodon @@ -92,7 +92,7 @@ Stummschalten Lautschalten Erwähnen - Verstecke Medien + Medien verstecken Drawer öffnen Speichern Profil bearbeiten @@ -101,7 +101,7 @@ Akzeptieren Ablehnen Suche - Gespeicherte Beiträge + Entwürfe Beitragssichtbarkeit Inhaltswarnung Emoji @@ -149,7 +149,7 @@ \n\nWeitere Informationen gibt es auf joinmastodon.org. Stelle Medienupload fertig - Lade hoch… + Lade hoch … Herunterladen Folgeanfrage zurückziehen? Willst du diesem Profil wirklich nicht mehr folgen? @@ -159,7 +159,7 @@ Nur Folgende: Nur für Folgende sichtbar Direkt: Nur für Erwähnte sichtbar Benachrichtigungen - Benachrichtigungseinstellungen + Benachrichtigungen Benachrichtigungen Benachrichtige mit Sound Benachrichtige mit Vibration @@ -197,7 +197,7 @@ Beiträge Fehler beim Synchronisieren Öffentlich - Ungelistet + Nicht gelistet Nur Folgende Schriftgröße Kleiner @@ -208,10 +208,10 @@ Neue Erwähnungen Benachrichtigungen über neue Erwähnungen Neue Folgende - Benachrichtigungen über neue Folgende + Benachrichtigunen über neue Folgende Geteilte Beiträge Benachrichtigungen wenn deine Beiträge geteilt werden - Favoriten + Favorisierte Beiträge Benachrichtigungen wenn deine Beiträge favorisiert werden %s hat dich erwähnt %1$s, %2$s, %3$s und %4$d andere @@ -227,7 +227,7 @@ to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html * the url can be changed to link to the localized version of the license. --> - Webseite des Projekts: + Website des Projekts: \n https://tusky.app Fehlermeldungen & Verbesserungsvorschläge:\n https://github.com/tuskyapp/Tusky/issues @@ -286,10 +286,10 @@ Später Neustarten Die Standard-Emojis deines Geräts - Die Blob-Emojis aus Android 4.4-7.1 + Die Blob–Emojis aus Android 4.4–7.1 Die Standard-Emojis von Mastodon Die aktuellen Emojis von Google - Download fehlgeschlagen + Download fehlgeschlagen. Bot %1$s ist umgezogen auf: An ursprüngliches Publikum teilen @@ -354,34 +354,33 @@ %s Stimme %s Stimmen - %s verbleibend endet um %s Geschlossen Abstimmen Eine Umfrage in der du abgestimmt hast ist vorbei - Eine Umfrage, die du erstellt hast, ist vorbei + Eine Umfrage die du erstellt hast ist vorbei - %d Tag - %d Tage + %d Tag verbleibend + %d Tage verbleibend - %d Stunde - %d Stunden + %d Stunde verbleibend + %d Stunden verbleibend - %d Minute - %d Minuten + %d Minute verbleibend + %d Minuten verbleibend - %d Sekunde - %d Sekunden + %d Sekunde verbleibend + %d Sekunden verbleibend Versteckte Domains Versteckte Domains %s verstecken %s nicht mehr versteckt Bist du dir sicher, dass du die Domain %s blockieren willst\? Nach der Blockierung wirst du nichts mehr von dieser Domain in öffentlichen Zeitleisten oder Benachrichtigungen sehen. Deine Follower von dieser Domain werden entfernt. - ganze Domain verbergen + Ganze Domain verbergen GIF-Avatare animieren Ganzes Wort Wenn das Schlagwort nur aus Buchstaben und Zahlen besteht, wird es nur angewendet, wenn es dem ganzen Wort entspricht @@ -404,7 +403,7 @@ 3 Tage 7 Tage Editieren - Tusky %s + test %s Umfrage hinzufügen Beiträge mit Inhaltswarnungen immer ausklappen Umfrage mit den Möglichkeiten: %1$s, %2$s, %3$s, %4$s; %5$s @@ -420,7 +419,7 @@ Geplante Beiträge Plane Beitrag Zurücksetzen - Audiodateien müssen kleiner als 40MB sein. + Audiodateien müssen kleiner als 40 MB sein. Lesezeichen Lesezeichen Lesezeichen @@ -452,7 +451,7 @@ Farbverlauf für versteckte Medien anzeigen Unten Oben - Ton einschalten %s + %s nicht mehr verstecken %s Boost %s Boosts @@ -461,5 +460,14 @@ Benachrichtigungen ausblenden Benachrichtigungen stummschalten von %s Benachrichtigungs-Ton einschalten von %s - Ton einschalten %s + %s nicht mehr verstecken + %d Sek. + %d St. + in %d T. + %d J. + Gespeichert! + Private Notiz über diesen Account + Titel der Hauptnavigation verstecken + Im Moment gibt es keine Ankündigungen. + Ankündigungen \ No newline at end of file diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index eeb2430f..964c1c89 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -376,28 +376,12 @@ - %s restas finiĝos je %s finiĝita Voĉdoni Enketo al kiu vi voĉdonis finiĝis Enketo kiu vi kreis finiĝis - - - - - - - - - - - - - - - - + Kaŝitaj domajnoj Kaŝitaj domajnoj Silentigi %s diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 1f9aefda..5ba0200d 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -16,7 +16,7 @@ No pudo abrirse el fichero. Se requiere permiso para acceder al almacenamiento. Se requiere permiso para descargar al almacenamiento. - No se pueden adjuntar imágenes y vídeos en el mismo estado + No se pueden adjuntar imágenes y vídeos en el mismo estado. La subida falló. Error al publicar. Inicio @@ -72,7 +72,7 @@ Reportar Borrar Enviar - Publicar + ¡Publicar! Reintentar Cerrar Perfil @@ -116,7 +116,7 @@ El usuario ya no está silenciado ¡Enviado! Respuesta enviada correctamente. - Indique la instancia + ¿Qué instancia\? ¿En qué estás pensando? Aviso de contenido Nombre @@ -300,8 +300,8 @@ <b>%1$s</b> Favoritos - %s Boost - %s Boosts + %s impulso + %s impulsos Impulsado por Marcado como favorito por @@ -330,20 +330,20 @@ Mostrar indicador de bots Votar - - + %d día restante + %d días restante - - + %d hora restante + %d horas restante - %d minuto - %d minutos + %d minuto restante + %d minutos restante - %d segundo - %d segundos + %d segundo restante + %d segundos restante %1$s • %2$s @@ -388,7 +388,6 @@ Redactar ¿Estás seguro de que quieres eliminar permanentemente todas tus notificaciones\? Acciones para la imagen %s - %s restante termina en %s Una encuesta en la que has votado ha terminado Una encuesta que has creado ha terminado @@ -475,4 +474,5 @@ Ocultar notificaciones Silenciar notificaciones desde %s Dejar de silenciar notificaciones desde %s + Ocultar el título de la barra de herramientas superior \ No newline at end of file diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index e59da251..511b0957 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -45,7 +45,7 @@ Edukirik ez. Arrastatu behera birkargatzeko! %s-(e)k zure tuta bultzatu du %s-(e)k zure tuta gogoko du - %s-(e)k jarraitu dizu + %s(e)k jarraitu zaitu \@%s salatu Informazio gehigarria? Erantzun azkarra @@ -75,7 +75,7 @@ Gogokoak Isilduak Blokeatuak - Eskariak + Eskakizunak Multimedia Nabigatzailean ireki Multimedia erantsi @@ -129,7 +129,7 @@ Mediaren igoera bukatzen Igotzen… Jaitsi - Jarraipen-eskaerari uko egin\? + Jarraipen-eskakizunari uko egin\? Kontu hau jarraitzeari utzi\? Tuta ezabatu\? Publikoa: Istorio publikoetan erakutsi @@ -156,7 +156,7 @@ Automatikoa Nabigatzailea Chromeko fitxak erabili - Tut egiteko botoia ezkutatu beherantz joaterakoan. + Tut egiteko botoia ezkutatu beherantz joaterakoan Denbora-lerro filtroak Fitxak Bultzadak erakutsi @@ -279,7 +279,7 @@ Ainguratu Sareko errore bat sortu da! Zure konexioa ziurta ezazu berriro, mesedez! Mezu Zuzenak - Kategoriak + Fitxak Lotuta Ezkutuko domeinuak Programatutako tutak @@ -291,7 +291,7 @@ Ezabatu eta zirriborroa berriro egin Ezkutuko domeinuak Galdeketa gehitu - %s isilarazi + Mututu %s Programatutako tutak Tuta programatu Berrezarri @@ -361,7 +361,7 @@ %1$s %1$s eta %2$s %1$s, %2$s eta %3$d gehiago - geienezko %1$d fitxa iritsita + gehienezko %1$d fitxa iritsita Media: %s Edukiaren abisua: %s Deskribapenik ez @@ -383,8 +383,8 @@ %s irudiarentzako ekintzak %1$s • %2$s - - + Boto %s + %s Boto %s amaitzen da Itxita @@ -392,20 +392,16 @@ Botoa eman duzun galdeketa amaitu da Sortu duzun galdeketa amaitu da - Egun %d - %d egun + Egun %d geratzen da + %d egun geratzen da - Ordu %d - %d ordu - - - Minutu %d - %d minutu + Ordu %d geratzen da + %d ordu geratzen da - Segundu %d - %d segundu + Segundu %d geratzen da + %d segundu geratzen da Jarraitu Itzuli @@ -439,7 +435,6 @@ Ireki bultzadaren egilea Denbora lerro publikoak Laster-markatuta - %s geratzen da Audioak 40MB baino gutxiago izan behar ditu. Aukeratu zerrenda Zerrenda @@ -450,13 +445,31 @@ Jarraitzeko eskaereri buruzko jakinarazpenak \@%s isildu\? \@%s blokeatu\? - Elkarrizketa isildu - %s -k zu jarraitzeko eskatu dizu + Mututu elkarrizketa + %s(e)k zu jarraitzeko eskatu dizu Traolak Ez erakutsi jakinarazpenak Desmututu %s - Pertsona %s - %s pertsona + Pertsona %1 + %2 pertsona + Ezkutatu goiko tresna-barraren izenburua + Erakutsi berrespen-abisua tuta bultzatu aurretik + Erakutsi esteken aurrebista denbora-lerroetan + Gaitu pasatze-keinua fitxetan zehar aldatzeko + + Minutu %d faltan + %d minutu faltan + + Gehitu traola + Azpia + Goia + Nabigatze posizio nagusia + Erakutsi gradiente koloretsua ezkutuko mediarentzako + jarraipena-eskaera + Desmututu elkarrizketa + Desmututu %s + Mututu %s(r)en jakinarazpenak + Desmututu %s(r)en jakinarazpenak \ No newline at end of file diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 80707e70..55eec785 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -1,57 +1,57 @@ - خطایی رخ داد + خطایی رخ داد. این نمی‌تواند خالی باشد. - دامنه وارد شده نامعتبر است - اهراز هویت با نمونه شکست خورد. - مرورگری برای استفاده یافت نشد - خطای احراز هویت ناشناس رخ داده است. - هویت احراز نشد - دریافت توکن ورود ناموفق بود + دامنهٔ نامعتبر وارد شده + احراز هویت با این نمونه شکست خورد. + مرورگری برای استفاده پیدا نشد. + خطای احراز هویت ناشناخته‌ای رخ داد. + احراز هویت رد شد. + دریافت ژتون ورود شکست خورد. وضعیت خیلی طولانی است! - پرونده باید کمتر از ۸ مگابایت باشد + پرونده باید کمتر از ۸ مگابایت باشد. پرونده ویدئویی باید کمتر از ۴۰ مگابایت باشد. - این نوع از پرونده نمی‌تواند بارگذاری شود - پرونده باز نشد - مجوز برای خواندن رسانه نیاز است + این گونهٔ پرونده نمی‌تواند بارگذاری شود. + این پرونده نتوانست گشوده شود. + نیاز به اجازهٔ خواندن رسانه است. نیاز به اجازهٔ ذخیرهٔ رسانه است. تصاویر و فیلم‌ها هر دو نمی‌توانند به یک وضعیت ضمیمه شوند. - بارگذاری ناموفق بود - خطا در ارسال بوق + بارگذاری شکست خورد. + خطای فرستادن بوق. خانه - اعلان‌ها - محلی + آگاهی‌ها + محلّی همگانی بوق - فرسته‌ها - با پاسخ‌‌ها - دنبال شونده‌ها - پیرو - پسندها - کاربرهای بی‌صدا - کاربرهای مسدود شده + فرسته + با پاسخ‌ + دنبال شونده + پی‌گیر + برگزیده‌ها + کاربران خموش + کاربران مسدود درخواست‌های پی‌گیری ویرایش نمایه‌تان - پیش‌نویس - مجوزها - %s تقویت شد + پیش‌نویس‌ها + پروانه‌ها + %s تقویت کرد محتوای حسّاس - پنهان کردن رسانه + رسانهٔ نهفته کلیک برای نمایش - نمایش بیشتر - نمایش کمتر - بازکردن + نمایش بیش‌تر + نمایش کم‌تر + گسترش بستن - چیزی اینجا نیست.برای تازه سازی، به پایین بکشید - %s بوق شما را تقویت کرد - %s بوق شما را پسندید - %s شما را دنبال می‌کند + این‌جا هیچ‌چیز نیست. برای تازه‌سازی، به پایین بکشید! + %s بوقتان را تقویت کرد + %s بوقتان را برگزید + %s پی‌گیرتان شد گزارش @%s - پیام‌های اضافی + نظرهای اضافی؟ پاسخ سریع پاسخ تقویت - مورد علاقه + برگزیدن بیش‌تر ایجاد ورود با ماستودون @@ -72,17 +72,17 @@ نمایه ترجیحات ترجیحات حساب - علاقه‌مندی‌ها + برگزیده‌ها کاربران خموش - کاربران مسدود شده + کاربران مسدود درخواست‌های پی‌گیری رسانه گشودن در مرورگر افزودن رسانه گرفتن عکس - اشتراک - بی‌صدا - لغو بی‌صدا + هم‌رسانی + خموشی + ناخموشی اشاره نهفتن رسانه گشودن کشو @@ -92,33 +92,33 @@ بازگشت پذیرش رد - جستجو + جست‌وجو پیش‌نویس نمایانی بوق - هشدار محتوی - صفحه کلیک شکلک - درحال دریافت %1$s - کپی لینک + هشدار محتوا + صفحه‌کلید اموجی + درحال بارگیری %1$s + رونوشت از پیوند هم‌رسانی نشانی بوق با… هم‌رسانی بوق با… هم‌رسانی رسانه با… فرستاده شد! - کاربر رفع انسداد شد - کاربر رفع بی‌صدا شد + کاربرنامسدود شد + کاربر ناخموش شد فرستاده شد! - پاسخ با موفق فرستاده شد‌. + پاسخ با موفّقیت فرستاده شد‌. کدام نمونه؟ چه خبر؟ - هشدار محتوی + هشدار محتوا نام نمایشی شرح حال - جستجو - نتیجه‌ای نیست - پاسخ … + جست‌وجو… + بدون هیچ نتیجه‌ای + پاسخ… آواتار - سرتیتر - یک نمونه چیست؟ - در حال اتصال … + سرایند + نمونه چیست؟ + در حال وصل شدن… نشانی یا دامنهٔ هر نمونه‌ای می‌تواند وارد شود، مثل mastodon.social, icosahedron.website, social.tchncs.de, و بیش‌تر!. \n \n اگر هنوز حسابی ندارید، می‌توانید نام نمونه مورد نظر را وارد کرده و در آن حسابی بسازید. @@ -130,23 +130,23 @@ در حال بارگذاری… بارگیری درخواست دنبال کردن را لغو می‌کنید؟ - لغو دنبال کردن این حساب - حذف این بوق + ناپیگیری این حساب؟ + حذف این بوق؟ عمومی: فرستادن به خط زمانی‌های عمومی - خارج از لیست: در خط زمانی عمومی نشان نده + فهرست‌نشده: نشان ندادن در خط زمانی‌های عمومی تنها دنبال‌کنندگان:پست فقط به دنبال‌کنندگان - مستقیم:پست فقط برای کاربران صدا زده شده - ویرایش اعلان‌ها - اعلان‌ها + مستقیم: فرستادن فقط برای کاربران اشاره‌شده + آگاهی‌ها + آگاهی‌ها هشدارها - اعلان با صدا + آگاهی با صدا آگاهی با لرزش - اعلان با نور + آگاهی با چراغ به من اطلاع بده زمانی که اشاره شدن دنبال‌شده - پست‌های تقویت شده من - پسند بوق‌هایم + فرسته‌هایم تقویت شدند + فرسته‌هایم برگزیده شدند ظاهر تم برنامه خط‌ زمانی‌ها @@ -159,7 +159,7 @@ پنهان کردن دکمه ایجاد هنگام پیمایش فیلتر کردن خط زمانی زبانه‌ها - نمایش بازبوق‌ها + نمایش تقویت‌ها نمایش پاسخ‌ها بارگیری پیش‌نمایش رسانه پراکسی @@ -172,7 +172,7 @@ در حال انتشار (همگام با کارساز) ناتوانی در هم‌گام‌سازی تنظیمات عمومی - لیست‌نشده + فهرست‌نشده فقط پی‌گیران اندازهٔ متن وضعیت کوچک‌ترین @@ -180,20 +180,20 @@ متوسط بزرگ بزرگ‌ترین - صدا زدن جدید - هشدار در مورد صدازدن جدید - پیروان جدید - هشدارها درمورد دنبال‌کنندگان جدید - تقویت - وقتی بوق‌های شما تقویت شد اعلان بده - مورد علاقه‌ها - زمانی که بوق‌های شما پسندیده شوند، اعلان دریافت کنید - %s شما را صدا زد - %1$s, %2$s, %3$s و %4$d دیگر - %1$s, %2$s, و %3$s + اشاره‌های جدید + آگاهی‌ها دربارهٔ اشاره‌های جدید + پی‌گیران جدید + آگاهی‌ها دربارهٔ پی‌گیران جدید + تقویت‌ها + آگاهی‌ها هنگام تقویت شدن بوق‌هایتان + برگزیدن‌ها + آگاهی‌ها هنگام برگزیده شدن بوق‌هایتان + %s به شما اشاره کرد + %1$s، %2$s، %3$s و %4$d دیگر + %1$s، %2$s و %3$s %1$s و %2$s %d برهم‌کنش جدید - حساب قفل شد + حساب قفل‌شده درباره تاسکی نرم‌افزاری آزاد است که تحت نگارش ۳ از پروانهٔ جامع همگانی گنو منتشر شده است. پروانه را می‌توانید از این‌جا ببینید: https://www.gnu.org/licenses/gpl-3.0.en.html - سایت پروژه + پایگاه وب پروژه : \n https://tusky.app - گزارش خطا و درخواست ویژگی: + گزارش مشکلات و درخواست ویژگی‌ها: \n https://github.com/tuskyapp/Tusky/issues - نمایه Tusky - هم‌رسانی محتوی بوق - هم‌رسانی لینک بوق + نمایهٔ تاسکی + هم‌رسانی محتوای بوق + هم‌رسانی پیوند بوق تصویرها - فیلم + ویدیو تقاضای پیگیری شد - در %dy - در %dd - در % dh - در %dm - در %ds - شما را دنبال می‌کند - همیشه مطالب حساس را نشان بده + در %d سال + در %d روز + در %d ساعت + در %d دقیقه + در %d ثانیه + پیگیرتان است + نمایش همیشگی محتوای حساس رسانه - پاسخ دادن به @%s - بارگیری بیشتر + در حال پاسخ به @%s + بار کردن بیش‌تر افزودن حساب - افزودن حساب جدید ماستودون + افزودن حساب ماستودون جدید فهرست‌ها - لیست‌ها - لیست خط زمانی - پست با حساب %1$s - ناتوان در تنظیم عنوان - توصیف برای کم‌بینایان\n(محدودیت نویسه %d) + فهرست‌ها + خط زمانی فهرست + در حال فرستادن با حساب %1$s + شکست در تنظیم عنوان + توصیف برای کم‌بینایان +\n(کران %d نویسه) تنظیم عنوان - حذف + برداشتن قفل حساب - به شما امکان می‌دهد بصورت دستی دنبال‌کنندگان را تایید کنید - ذخیره به عنوان پیش‌نویس؟ + لازم است پی‌گیران را دستی تأیید کنید + ذخیرهٔ پیش‌نویس؟ در حال فرستادن بوق… خطای فرستادن بوق در حال فرستادن بوق‌ها فرستادن لغو شد - رونوشتیی از بوق در پیش‌نویس‌هایتان ذخیره شد + رونوشتی از بوق در پیش‌نویس‌هایتان ذخیره شد ایجاد - سرور شما %s هیچ شکلک سفارشی ندارد - به کلیپ بورد کپی شد - قالب شکلک - پیشفرض سیستم - شما ابتدا باید این شکلک‌ها را دریافت کنید - اجرای جستجو - باز/بستن همه وضعیت‌ها - بوق را باز کن - برنامه به شروع مجدد نیاز دارد - شما برای اعمال این تغییرات به شروع مجدد برنامه نیاز دارید + نمونه‌تان %s هیچ اموجی سفارشی‌ای ندارد + در تخته‌گیره رونوشت شد + سبک اموجی + پیش‌گزیدهٔ سامانه + نخست باید این مجموعه‌های اموجی را بارگیری کنید + در حال جست‌وجو… + گسترده/جمع کردن تمام وضعیت‌ها + گشودن بوق + نیاز به آغاز دوبارهٔ کاره + برای اعمال این تغییرات، نیاز به شروع دوبارهٔ تاسکی دارید بعداً - شروع مجدد - شکلک‌های پیش‌فرض دستگاه شما تنظیم شدند - شکلک‌های اندروید ۴.۴ تا ۷.۱ - شکلک‌های استاندارد ماستدون تنظیم شدند + آغاز دوباره + مجموعه اموجی پیش‌گزیدهٔ افزاره‌تان + اموجی‌های اندروید ۴.۴ تا ۷.۱ + مجموعه اموجی استاندارد ماستودون شکست در بارگیری بات %1$s منتقل شد به: تقویت برای مخاطب اصلی - لغو تقویت - Tusky شامل کد و دارایی‌های پروژه‌های منبع‌باز زیر است: - تحت مجوز آپاچی (رونوشت در ادامه) - متاداده نمایه + ناتقویت + تاسکی کد و دارایی‌هایی از پروژه‌های نرم‌افزار آزاد زیر دارد: + ذیل پروانهٔ آپاچی (رونوشت در ادامه) + فرادادهٔ نمایه افزودن داده برچسب محتوا استفاده از زمان مطلق - اطلاعات زیر ممکن است به طور ناقص نمایه کاربر را نشان دهد. برای دیدن نمایه کامل در مرورگر، لمس کنید. + ممکن است اطلاعات زیر نمایهٔ کاربر را ناقص نشان دهد. برای گشودن نمایهٔ کامل در مرورگر، لمس کنید. برداشتن سنجاق سنجاق کردن یک خطای شبکه رخ داد! لطفا اتصال خود را بررسی و دوباره تلاش کنید! پیام‌های مستقیم زبانه‌ها سنجاق‌شده - دامنه‌های پنهان + دامنه‌های نهفته \@%s - چیزی اینجا نیست. - حذف بازبوق - برداشتن پسند - پاک کردن و بازنویسی - دامنه‌های پنهان + این‌حا هیچ‌چیزی نیست. + برداشتن تقویت + برداشتن برگزیدگی + حذف و بازنویسی + دامنه‌های نهفته افزودن نظرسنجی - بی‌صدا کردن %s - افزوده زبانه + خموشی %s + افزودن زبانه پیوندها اشاره‌ها هشتگ‌ها - بازکردن حساب بازبوق کننده - نمایش بازبوق‌ها - نمایش پسندیده‌ها + گشودن تقویت‌کنندهٔ بوق + نمایش تقویت‌ها + نمایش برگزیده‌ها هشتگ‌ها اشاره‌ها پیوندها - بازکردن رسانه #%d + گشودن رسانه #%d گشودن به عنوان %s هم‌رسانی به عنوان … بارگیری رسانه در حال بارگیری رسانه - %s ناپنهان + %s نانهفته می‌خواهید این بوق را پاک و بازنویسی کنید؟ نهفتن تمام دامنه پایان نظرسنجی‌ها صافی‌ها استفاده از طرح سیستم زبان - نمایش نشان برای بات‌ها + نمایش نشانگر برای بات‌ها پویانمایی آواتار gif نظرسنجی‌ها - اعلان‌ها درباره نظرسنجی‌هایی که پایان یافته‌اند - Tusky (تاسکی) %s - %dy - %dd - %dh - %dm - %dث - همواره بوق‌هایی که دارای محتوای حساس هستند را گسترش بده - خط زمانی عمومی - گفتگوها - افزودن صافی - ویرایش صافی + آگاهی‌ها دربارهٔ نظرسنجی‌های پایان‌یافته + تاسکی %s + %d سال + %d روز + %d ساعت + %d دقیقه + %d ثانیه + گسترش همیشگی بوق‌های علامت‌خورده با هشدار محتوا + خط زمانی‌های عمومی + گفت‌وگوها + افزودن پالایه + ویرایش پالایه برداشتن - به‌روزرسانی - تمام کلمه - ناتوانی در ایجاد سیاهه - ناتوانی در تغییر نام سیاهه + به‌روز رسانی + تمام واژه + ناتوانی در ایجاد فهرست + ناتوانی در تغییر نام فهرست ناتوانی در حذف فهرست ایجاد یک فهرست تغییر نام فهرست حذف فهرست - ویرایش سیاهه - جستجو بین افرادی که دنبال می‌کنید + ویرایش فهرست + جست‌وجوی افرادی که پی می‌گیرید افزودن حساب به فهرست - حذف حساب از سیاهه - مجموعه شکلک‌های جاری گوگل - CC-BY 4.0 - CC-BY-SA 4.0 + برداشتن حساب از فهرست + مجموعه اموجی کنونی گوگل + نگارش ۴٫۰ CC-BY + نگارش ۴٫۰ CC-BY-SA - %1$s پسند - %1$s پسند + %1$s برگزیدن + %1$s برگزیدن - %s بازبوق - %s بازبوق + %s تقویت + %s تقویت - بازبوق توسط - پسننده‌شده توسط + تقویت‌شده به دست + برگزیده به دست %1$s %1$s و %2$s - %1$s، %2$s و %3$d بیشتر + %1$s، %2$s و %3$d بیش‌تر رسیده به بیشینهٔ %1$d زبانه رسانه: %s هشدار محتوا: %s - بدون توضیحات + بدون هیچ توضیحی بازبوقیده - پسندیده + برگزیده عمومی فهرست‌نشده پی‌گیران مستقیم - نظرسنجی با انتخاب‌ها: %1$s، %2$s، %3$s، %4$s؛ %5$s - نام سیاهه + نظرسنجی با گزینه‌ها: %1$s، %2$s، %3$s، %4$s؛ %5$s + نام فهرست هشتگ بدون # - پاک کردن - صافی + پاک‌سازی + پالایش اعمال ایجاد بوق ایجاد مطمئنید می‌خواهید تمام آگاهی‌هایتان را برای همیشه پاک کنید؟ - %s باقی مانده - پایان یافته در %s - بسته شده - رای + پایان در %s + بسته + رأی یک نظرسنجی که در آن رأی دادید، تمام شد یک نظرسنجی که ساختید، تمام شد - %d روز - %d روز + %d روز مانده + %d روز مانده - %d ساعت - %d ساعت + %d ساعت مانده + %d ساعت مانده - %d دقیقه - %d دقیقه + %d دقیقه مانده + %d دقیقه مانده ادامه - قبل - اتمام - با موفقیت گزارش شد @%s - نظرات بیشتر + بازگشت + تمام + ‫@%s با موفّقیت گزارش شد + نظرهای اضافی هدایت به %s - ناتوانی در گزارش + شکست در گزارش شکست در واکشی وضعیت‌ها حساب‌ها - ناتوانی در جستجو - نمایش فیلتر اعلانات + شکست در جست‌وجو + نمایش پالایهٔ آگاهی‌ها نظرسنجی ۵ دقیقه ۳۰ دقیقه @@ -413,14 +413,14 @@ ویرایش بوق‌های زمان‌بسته ویرایش - بوق‌های زمان‌بندی‌شده + بوق‌های زمان‌بسته بوق زمان‌بسته بازنشانی - مطمئنید می‌خواهید تمام %s را مسدود کنید؟ محتوای آن دامنه را در هیچ‌یک از خط زمانی‌ها یا در آگاهی‌هایتان نخواهید دید. پیروانتان از آن دامنه، برداشته خواهند شد. + مطمئنید می‌خواهید تمام %s را مسدود کنید؟ محتوای آن دامنه را در هیچ‌یک از خط زمانی‌ها یا در آگاهی‌هایتان نخواهید دید. پی‌گیرانتان از آن دامنه، برداشته خواهند شد. هنگامی که کلیدواژه یا عبارت، فقط حروف‌عددی باشد، فقط اگر با تمام واژه مطابق باشد، اعمال خواهد شد عبارت پالایش - " %1$s • %2$s" - گزارش به ناظم‌های کارسازتان ارسال خواهد شد. می‌توانید توضیحی در باب چرایی گزارش این حساب در زیر بنویسید: + . %1$s • %2$s + گزارش به ناظرهای کارسازتان ارسال خواهد شد. در زیر می‌توانید توضیحی در باب چرایی گزارش این حساب بنویسید: این حساب از کارسازی دیگر است. رونوشتی ناشناس از گزارش، به آن‌جا نیز ارسال شود؟ خطا در یافتن فرستهٔ %s قدرت‌گرفته از تاسکی @@ -443,17 +443,17 @@ هشتگ‌ها افزودن هشتگ - آگاهی‌ها در مورد درخواست‌های پی‌گیری + آگاهی‌ها دربارهٔ درخواست‌های پی‌گیری درخواست‌های پی‌گیری درخواست پی‌گیری خموشی @%s؟ انسداد @%s؟ ناخموشی گفت‌وگو خموشی گفت‌وگو - %s می‌خواهد دنبالتان کند + %s می‌خواهد پی‌گیرتان شود - %d ثانیه - %d ثانیه + %d ثانیه مانده + %d ثانیه مانده پایین بالا @@ -464,4 +464,14 @@ خموشی آگاهی‌ها از %s ناخموشی آگاهی‌ها از %s ناخموشی %s + + %s رأی + %s رأی + + نهفتن عنوان نوارابزار بالایی + کنش‌ها برای تصویر %s + ذخیره شد! + یادداشت خصوصیتان دربارهٔ این حساب + هیچ اعلامیه‌ای وجود ندارد. + اعلامیه‌ها \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 09f96374..48f68f0c 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -372,7 +372,6 @@ Désirez-vous nettoyer toutes vos notifications de façon permanente \? Effacer et ré-écrire Effacer et ré-écrire ce pouet \? - %s restant Termina à %s Terminé Voter @@ -381,20 +380,20 @@ Notifications pour les sondages terminés Un sondage que vous avez créé est terminé - %d jour - %d jours + %d jour restant + %d jours restant - %d heure - %d heures + %d heure restant + %d heures restant - %d minute - %d minutes + %d minute restant + %d minutes restant - %d seconde - %d secondes + %d seconde restant + %d secondes restant Activer l’animation des avatars Actions pour l’image %s @@ -415,7 +414,7 @@ Échec du signalement Échec de récupération des statuts Le rapport sera envoyé aux modérateur·rice·s de votre instance. Vous pouvez expliquer pourquoi vous signalez le compte ci-dessous : - Êtes-vous sûr⋅e de vouloir bloquer %s en entier? Vous ne verrez plus de contenu provenant de ce domaine, ni dans fils publics, ni dans vos notifications. Vos abonné·e·s utilisant ce domaine seront retiré·e·s. + Êtes-vous sûr⋅e de vouloir bloquer %s en entier \? Vous ne verrez plus de contenu provenant de ce domaine, ni dans les fils publics, ni dans vos notifications. Vos abonné·e·s utilisant ce domaine seront retiré·e·s. Terminé Le compte provient d’un autre serveur. Envoyez également une copie anonyme du rapport \? Montrer le filtre des notifications @@ -476,4 +475,13 @@ Silencier les notifications de %s Ne plus silencier les notifications de %s Ne plus masquer %s + + %s personne + %s personnes + + Cacher le titre de la barre d’outils supérieure + + %s voix + %s voix + \ No newline at end of file diff --git a/app/src/main/res/values-ga/strings.xml b/app/src/main/res/values-ga/strings.xml index da980f68..fa8b8562 100644 --- a/app/src/main/res/values-ga/strings.xml +++ b/app/src/main/res/values-ga/strings.xml @@ -297,11 +297,11 @@ An bhfuil tú cinnte gur mhaith leat do chuid fógraí go léir a ghlanadh go buan\? Tá deireadh le vótaíocht a chruthaigh tú - $d nóiméad - $d nóiméad - $d nóiméad - $d nóiméad - $d nóiméad + D\'imigh $d nóiméad + D\'imigh $d nóiméad + D\'imigh $d nóiméad + D\'imigh $d nóiméad + D\'imigh $d nóiméad Theip ar stádas a fháil Seolfar an tuarascáil chuig do mhodhnóir freastalaí. Féadfaidh tú míniú a thabhairt ar an bhfáth go bhfuil tú ag tuairisciú an chuntais seo thíos: @@ -407,7 +407,6 @@ %s daoine %s daoine - D\'imigh %s foircinn ag %s dúnta Vóta @@ -420,18 +419,18 @@ %d lá - %d uair - %d uair an chloig - %d uair an chloig - %d uair an chloig - %d uair an chloig + D\'imigh %d uair + D\'imigh %d uair an chloig + D\'imigh %d uair an chloig + D\'imigh %d uair an chloig + D\'imigh %d uair an chloig - %d soicind - %d soicind - %d soicind - %d soicind - %d soicind + D\'imigh %d soicind + D\'imigh %d soicind + D\'imigh %d soicind + D\'imigh %d soicind + D\'imigh %d soicind Lean ar aghaidh Ar ais diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml index be0a5b0a..ed594dc7 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -34,7 +34,7 @@ Ath-ghairm Dùin TÙT - Sguab às + Cuir às Cuir às agus ath-sgrìobhadh Thoir cunntas air Dì-bhac @@ -50,4 +50,6 @@ Dreachdan Prìomhaich Fiosan + Fiosan + Prìomhaich \ No newline at end of file diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index ec584ff7..6c652ead 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -208,17 +208,16 @@ 30 मिनिट 5 मिनट - %d घंटा - %d घंटे + %d घंटा शेष + %d घंटे शेष - %d दिन - %d दिन + %d दिन शेष + %d दिन शेष आपके द्वारा बनाया गया एक जनमत समाप्त हो गया है आपके द्वारा मतदान किया गया जनमत समाप्त हो गया है %s समाप्त होगा - %s शेष %s व्यक्ति %s लोग @@ -396,4 +395,12 @@ %s ने आपको फ़ॉलो किया बूस्ट दिखाएं बूस्ट दिखाएं + + %1$s ने पसंद किया + %1$s ने पसंद किया + + + %d सेकेंड शेष + %d सेकेंड शेष + \ No newline at end of file diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 65c7dea5..8384ed34 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -1,7 +1,7 @@ Hiba történt. - Hálózati hiba történt! Kérjük, ellenőrizze a kapcsolatot, és próbálja meg újra! + Hálózati hiba történt! Kérjük, ellenőrizd a kapcsolatot, és próbálja meg újra! Ez nem lehet üres. Helytelen domén Sikertelen bejelentkezés ezen a szerveren. @@ -18,18 +18,18 @@ Média tárolási engedély szükséges. Képek és videók egyszerre nem csatolhatók ugyanazon tülköléshez. Feltöltés sikertelen. - A tülk elküldése nem sikerult. + Nem sikerült elküldeni a tülköt.. Kezdőlap Értesítések Helyi - Egyesített + Föderáció Közvetlen üzenetek Fülek Tülk Posztok Válaszokkal Rögzített - Követ + Követett Követő Kedvencek Némított felhasználók @@ -37,7 +37,7 @@ Követési kérelmek Profilod szerkesztése Piszkozatok - Licencek + Licenszek \@%s %s megtolta Kényes tartalom @@ -49,23 +49,23 @@ Összecsukás Nincs itt semmi. Üres tartalom. Húzd le a frissítéshez! - %s megolta a tülkölésed + %s megtolta a tülködet %s kedvencnek jelölte tülködet - %s követett + %s bekövetett \@%s jelentése Egyéb megjegyzés? Gyors válasz Válasz - Rebloggol - Kedvenc + Megtolás + Kedvencnek jelölés Több Szerkesztés - Bejelentkezés Mastodon-al + Bejelentkezés Mastodon-nal Kijelentkezés Biztosan ki szeretnél jelentkezni a következőből: %1$s? Követés Követés vége - Blokkol + Letiltás Letiltás feloldása Megtolások elrejtése Megtolások mutatása @@ -73,31 +73,31 @@ Törlés TÜLK TÜLK! - Próbálja újra - Bezár + Próbáld újra + Bezárás Profil Beállítások Fiókbeállítások Kedvencek Némított felhasználók - Blokkolt felhasználók + Letiltott felhasználók Követési kérelmek Média Megnyitás böngészőben Média csatolása Kép készítése - Megoszt - Némít + Megosztás + Némítás Némítás feloldása Megemlítés Média elrejtése - Drawer megnyitása + Menü megnyitása Mentés Profil szerkesztése Szerkesztés Visszavonás - Elfogad - Elutasít + Elfogadás + Elutasítás Keresés Piszkozatok Tülkök láthatósága @@ -108,7 +108,7 @@ Említések Hashtagek Kedvencek megjelenítése - Címkék + Hashtagek Említések Linkek %1$s letöltése @@ -122,15 +122,15 @@ Felhasználó némítása feloldva Elküldve! Válasz sikeresen elküldve. - Mely szerver\? - Mire gondolsz\? - Tartalom Figyelmeztető + Melyik szerver\? + Mi jár a fejedben\? + Tartalom figyelmeztetés Megjelenítési név Bemutatkozás Keresés… Nincs találat Válasz… - Avatár + Profilkép Fejléc Mi az a szerver\? Csatlakozás… @@ -151,18 +151,18 @@ Listázatlan: Nem jelenik meg a nyilvános idővonalon Csak követőknek: Tülkölés csak követőknek Közvetlen: Tülkölés csak a megemlített felhasználóknak - Értesítések szerkesztése + Értesítések Értesítések Figyelmeztetések Értesítés hanggal Értesítés rezgéssel Értesítés fénnyel Értesítsen, ha - említett + megemlítettek bekövettek tülkömet megtolták tülkömet kedvenccé tették - Kinézet + Megjelenés Idővonalak Sötét Világos @@ -170,10 +170,10 @@ Automatikus naplementekor Böngésző Linkek megnyitása applikáción belül - Szerkesztési gomb elérejtése görgetés közben + Szerkesztés gomb elrejtése görgetés közben Idővonal szűrése Fülek - Rebloggolások mutatása + Megtolások mutatása Válaszok mutatása Média előnézet mutatása Proxy @@ -181,13 +181,13 @@ HTTP proxy engedélyezése HTTP proxy szerver HTTP Proxy port - Alapértelmezett poszt láthatóság - Minden médiát jelölj meg érzékenynek + Tülkök alapértelmezett láthatósága + Minden média szenzitívnek jelölése A beállítások szinkronizálása nem sikerült Nyilvános Listázatlan Csak követőknek - Státusz szöveg mérete + Tülkölés szöveg mérete Legkisebb Kicsi Közepes @@ -196,12 +196,12 @@ Új említések Értesítések új említések esetén Új követők - Értesítések az új követők esetén - Rebloggok - Értesítések posztjaid rebloggolása esetén + Értesítések új követőkről + Megtolások + Értesítések posztjaid megtolása esetén Kedvencek Értesítések mikor tülkjeidet kedvencnek jelölik - %s megemített téged + %s megemlített téged %1$s, %2$s, %3$s és még %4$d %1$s, %2$s meg %3$s %1$s és %2$s @@ -220,7 +220,7 @@ Hibajelentés & új funkciók igénylése: \n https://github.com/tuskyapp/Tusky/issues - Tusky Profilja + Tusky profilja Tülk tartalmának megosztása Tülk linkjének megosztása Képek @@ -247,7 +247,7 @@ A %s szervernek nincsenek egyedi emoji-jai Vágólapra másolva Emoji stílus - Rendszer alapértelmezett + Rendszer alapértelmezés Először le kell töltened ezeket az emoji készleteket Keresés… Tülk megnyitása @@ -255,9 +255,9 @@ A beállítások érvényesítéséhez újra kell indítani a Tusky-t Később Újraindítás - Az eszközöd alap emoji készlete + Az eszközöd alapértelmezett emoji készlete Az Android 4.4-7.1 Blob emoji-jai - Mastodon alap emoji készlet + Mastodon alapértelmezett emoji készlet Letöltés sikertelen Bot %1$s elköltözött: @@ -269,7 +269,7 @@ Címke Tartalom Abszolút idő használata - Az alábbi felhasználói profil adatok lehet, hogy hiányosak. Kattints ide a teljes profil böngészőben való megnyitásához. + Az alábbi felhasználói profil adatok hiányosak lehetnek. Kattints ide a teljes profil böngészőben való megnyitásához. Megtolta Kedvencnek jelölte %1$s és %2$s @@ -285,14 +285,14 @@ Média letöltése Média megosztása következővel… Törlöd és újraírod ezt a tülköt\? - befejeződött a szavazás + befejeződött egy szavazás Szűrők Rendszer téma használata Nyelv Közzététel (szerverrel szinkronizált) Szavazások - Értesítés a befejezett szavazásokról - Tusky ingyenes és nyílt forráskódú szoftver. A GNU General Public License Version 3 érvényes rá, amit itt tekinthets meg: https://www.gnu.org/licenses/gpl-3.0.en.html + Értesítés befejezett szavazásokról + Tusky ingyenes és nyílt forráskódú szoftver. A GNU General Public License Version 3 érvényes rá, amit itt tekinthetsz meg: https://www.gnu.org/licenses/gpl-3.0.en.html %dé múlva %dn múlva %dó múlva @@ -301,7 +301,7 @@ %dn %dó %dp - %dm + %dmp Válasz @%s részére Nyilvános idővonalak Beszélgetések @@ -309,7 +309,7 @@ Szűrő szerkesztése Eltávolítás Frissítés - Szűrőkifejezés + Szűrendő kifejezés Nem sikerült a lista létrehozása Nem sikerült a lista átnevezése Nem sikerült a lista törlése @@ -345,7 +345,7 @@ Minden tülk kibontása/összecsukása Google jelenlegi emoji készlete Megtolás az eredeti közönségnek - Megtolás törlése + Megtolás visszavonása Apache licensz alatt Kitűzés eltávolítása Kitűzés @@ -372,7 +372,7 @@ Szűrés Alkalmaz Tülk Szerkesztése - Szerkeszt + Szerkesztés Biztos, hogy minden értesítésedet véglegesen törlöd\? Műveletek a %s képpel %1$s • %2$s @@ -380,27 +380,26 @@ %s szavazat %s szavazat - %s maradt vége %s véget ért Szavazás Egy szavazás véget ért, melyben részt vettél Egy szavazás véget ért, melyet te hoztál létre - %d nap - %d nap + %d nap maradt + %d nap maradt - %d óra - %d óra + %d óra maradt + %d óra maradt - %d perc - %d perc + %d perc maradt + %d perc maradt - %d másodperc - %d másodperc + %d másodperc maradt + %d másodperc maradt Folytatás Vissza @@ -436,9 +435,9 @@ Visszaállítás Nem találjuk ezt a posztot %s Könyvjelzők - Könyvjelző + Könyvjelzőzés Könyvjelzők - Tusky által hatjva + Tusky által hajtva Könyvjelzőzve Lista kiválasztása Lista @@ -461,15 +460,16 @@ Fent Fő navigálási pozíció Színes homály mutatása rejtett médiánál - követési kérelem + követni szeretnének Értesítések elrejtése Letiltsuk @%s -t\? Elnémítsuk @%s fiókot\? Beszélgetés némításának feloldása Beszélgetés némítása - Némítás vége %s + %s némításának feloldása Értesítések némítása tőle %s - Némítás vége %s + %s némításának feloldása Értesítések némításának feloldása tőle %s %s kéri, hogy követhessen + Felső eszköztár címének elrejtése \ No newline at end of file diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index 7d1088b9..9ce6c6cc 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -153,7 +153,7 @@ Aðvörun vegna efnis Birtingarnafn Æviágrip - Leita... + Leita… Engar niðurstöður Svara… Auðkennismynd @@ -377,7 +377,6 @@ %s atkvæði %s atkvæði - %s eftir lýkur %s lokað Greiða atkvæði @@ -423,4 +422,20 @@ %s Endurbirting %s Endurbirtingar + + %d sekúnda eftir + %d sekúndur eftir + + + %d mínúta eftir + %d mínútur eftir + + + %d klukkustund eftir + %d klukkustundir eftir + + + %d dagur eftir + %d dagar eftir + \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 6a881e0b..0c74ad9d 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -141,12 +141,12 @@ Cos\'è un\'istanza? Connessione… L\'indirizzo o il dominio di qualsiasi istanza può essere inserito qui, come mastodon.social, icosahedron.website, social.tchncs.de, e altro! -\n -\nSe non hai ancora un account, puoi inserire il nome di un\'istanza alla quale vuoi iscriverti e creare un account. -\n +\n +\nSe non hai ancora un account, puoi inserire il nome di un\'istanza alla quale vuoi iscriverti e creare un account. +\n \nUn\'istanza è il luogo dove l\'account è custodito, ma puoi facilmente comunicare e seguire gente su altre istanze come se fossero sullo stesso sito. -\n -\nPiù info possono essere trovate su joinmastodon.org. +\n +\nPiù info possono essere trovate su joinmastodon.org. Terminando il caricamento dei media Caricamento… Scarica @@ -371,7 +371,6 @@ %s voto %s voti - %s rimasti termina alle %s terminato Vota @@ -379,7 +378,7 @@ Domini nascosti Silenzia %s %s mostrati - Sei sicuro di voler bloccare tutto %s\? Non vedrai nessun contenuto da quel dominio in nessuna timeline pubblica o nelle tue notifiche. I tuoi seguaci che stanno in quel dominio saranno rimossi + Sei sicuro di voler bloccare tutto %s\? Non vedrai nessun contenuto da quel dominio in nessuna timeline pubblica o nelle tue notifiche. I tuoi seguaci che stanno in quel dominio saranno rimossi. Nascondi l\'intero dominio Le votazioni sono finite Mostra le animazioni delle GIF negli avatar @@ -403,16 +402,16 @@ Un sondaggio che hai votato è terminato Un sondaggio che hai creato è terminato - %d giorno - %d giorni + %d giorno rimasti + %d giorni rimasti - %d ora - %d ore + %d ora rimasti + %d ore rimasti - %d minuto - %d minuti + %d minuto rimasti + %d minuti rimasti %d secondo diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 3bc28548..37ecd192 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -332,19 +332,18 @@ %s票 - 残り%s 投票 - %d日 + 残り%d日 - %d時間 + 残り%d時間 - %d分 + 残り%d分 - %d秒 + 残り%d秒 非表示のドメイン 非表示のドメイン diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index d4bac917..3c10f80b 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -34,7 +34,7 @@ Jewweq JEWWEQ! Ɛreḍ tikkelt-nniḍen - Derreɛ + Mdel Amaɣnu Ismenyifen Ldi deg uminig @@ -174,23 +174,22 @@ %s n wedɣar %s n yedɣaren - %s id yugran ad ifak deg %s ifuk Dɣer Ifuk, tura kan, yiwen wedɣar t tteki-iḍ degs Ifukk yiwen wedɣar id snulfaḍ - %d n wass - %d n wussan + %d n wass id yugran + %d n wussan id yugran - %d wesrag - %d n yisragen + %d wesrag id yugran + %d n yisragen id yugran - %d n tasdidt - %d n tisdidin + %d n tasdidt id yugran + %d n tisdidin id yugran Kemmel Uɣal @@ -232,8 +231,8 @@ deg %dtsd deg %dtsn - %d n tasint - %d n tasinin + %d n tasint id yugran + %d n tasinin id yugran Agbur amḥulfu Creḍ allal n teywalt amzun d amḥulfu diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 1d59f76a..4689c5af 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -374,7 +374,6 @@ %s 명 참여 - %s 남음 %s에 종료 마감됨 투표 @@ -382,16 +381,16 @@ 당신이 시작한 투표가 종료되었습니다 - %d일 + %d일 남음 - %d시간 + %d시간 남음 - %d분 + %d분 남음 - %d초 + %d초 남음 다음 이전 diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index 4e5f45aa..72119eef 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -22,7 +22,7 @@ ആധികാരികത ഉറപ്പുവരുത്താനായില്ല. ഒരു പ്രവേശന ടോക്കൺ ലഭ്യമാക്കുന്നതിൽ പരാജയപ്പെട്ടു. ഈ സ്റ്റാറ്റസ് വളരെ നീളമേറിയതാണ്! - ഫയൽ 8എംബിയിലും ചെറുതായിരിക്കണം. + ഫയൽ 8 എംബിയേക്കാളും ചെറുതായിരിക്കണം. ചലച്ചിത്ര ഫയലുകൾ 40എംബിയിലും ചെറുതായിരിക്കണം. ഇത്തരം ഫയൽ അപ്‌ലോഡ് ചെയ്യാൻ സാധിക്കില്ല. ഈ ഫയൽ തുറക്കാനായില്ല. @@ -44,8 +44,8 @@ പിന്തുടരലുകൾ പിന്തുടരുന്നവർ നിശ്ശബ്ദരാക്കിയ ഉപയോക്താക്കൾ - തടയപ്പെട്ട ഉപയോക്താക്കൾ - പിന്തുടാരാനുള്ള അപേക്ഷകൾ + നിരോധിച്ച ഉപയോഗതാക്കൾ + പിന്തുടാനുള്ള അപേക്ഷകൾ പ്രൊഫൈൽ തിരുത്തുക മറുപടി ബൂസ്റ്റ് @@ -56,7 +56,7 @@ പിന്തുടരുക പിന്‍തുടരുന്നത് അവസാനിപ്പിക്കുക വീണ്ടും ശ്രമിക്കുക - പിന്തുടാനുള്ള അപേക്ഷകൾ + പിന്‍തുടരുവാനുള്ള അഭ്യര്‍ത്ഥനകള്‍ മറച്ചുവെച്ച ഡൊമൈനുകൾ പിന്തുടരുന്നവർ പുതിയത് ലഭിക്കാൻ താഴേക്ക് വലിക്കുക @@ -74,9 +74,9 @@ പുനഃക്രമീകരിക്കുക ബ്രൗസറിൽ തുറക്കുക മാധ്യമം - മറച്ചുവെച്ച ഡൊമൈനുകൾ - തടയപ്പെട്ട ഉപയോക്താക്കൾ - നിശ്ശബ്ദരാക്കിയ ഉപയോക്താക്കൾ + മറയ്ക്കപ്പെട്ട ഡൊമൈനുകൾ + നിരോധിച്ച ഉപയോഗതാക്കൾ + നിശ്ശബ്ദമാക്കിയ ഉപയോഗതാക്കൾ ബൂക്കമാർക്ക് ഇഷ്ടപ്പെട്ടത് പ്രൊഫൈൽ @@ -108,4 +108,7 @@ മുന്‍നിശ്ചയിച്ച ടൂറ്റ്‌സ് കരടുകൾ തിരുത്ത് + അറിയിപ്പുകൾ + ടാബുകൾ + അറിയിപ്പുകൾ \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 3b0898f2..d24e48c5 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -368,10 +368,9 @@ Weet je zeker dat je alle meldingen permanent wilt verwijderen\? %1$s • %2$s - - + %s stem + %s stemmen - %s over eindigt op %s gesloten Stemmen @@ -391,20 +390,20 @@ Poll met keuzes: %s, %s, %s, %s; %s Acties voor afbeelding %s - %d dag - %d dagen + %d dag over + %d dagen over - %d uur - %d uur + %d uur over + %d uur over - %d minuut - %d minuten + %d minuut over + %d minuten over - - + %d seconde over + %d seconden over Doorgaan Terug diff --git a/app/src/main/res/values-no-rNB/strings.xml b/app/src/main/res/values-no-rNB/strings.xml index 5de51558..d259e5e0 100644 --- a/app/src/main/res/values-no-rNB/strings.xml +++ b/app/src/main/res/values-no-rNB/strings.xml @@ -343,7 +343,6 @@ %s stemme %s stemmer - %s igjen avsluttes %s stengt Stem @@ -366,20 +365,20 @@ En avstemming du har stemt på er avsluttet En avstemming du opprettet er avsluttet - %d dag - %d dager + %d dag igjen + %d dager igjen - %d time - %d timer + %d time igjen + %d timer igjen - %d minutt - %d minutter + %d minutt igjen + %d minutter igjen - %d sekund - %d sekunder + %d sekund igjen + %d sekunder igjen Handlinger for bilde %s Animer GIF-avatarer @@ -464,4 +463,8 @@ Fjern demping av varsler fra %s Fjern demping av %s Skjul tittelen på den øverste verktøylinjen + Lagret! + Ditt private notat om denne kontoen + Det er ingen kunngjøringer. + Kunngjøringer \ No newline at end of file diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml index c52203f3..2ffd91ed 100644 --- a/app/src/main/res/values-oc/strings.xml +++ b/app/src/main/res/values-oc/strings.xml @@ -332,8 +332,8 @@ 3%1$s4 Favorits - - + %s partatge + %s partatges Partejat per Aimat per @@ -363,10 +363,9 @@ Mostrar coma indicator pels robòts Netejar totas las notificacions d’un biais permanent \? - - + %s vòte + %s votes - %s restant S’acaba a %s acabat Votar @@ -377,20 +376,20 @@ Un sondatge ont avètz votat es acabat Un sondatge qu’avètz creat es acabat - %d jorn - %d jorns + %d jorn restant + %d jorns restant - %d ora - %d oras + %d ora restant + %d oras restant - %d minuta - %d minutas + %d minuta restant + %d minutas restant - - + %d segonda restant + %d segondas restant Accions per l’imatge %s Activar l’animacion dels avatars @@ -450,4 +449,8 @@ L’interval minimum de planificacion sus Mastodon e de 5 minutas. Demandas d’abonament Etiquetas + Desactivar las notificacions per %s + Activar las notificacions per %s + Amagar pas mai a %s + %s a demandat a vos seguir \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 8303a486..5a6ec295 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -289,7 +289,7 @@ Ukryte domeny Dodaj głosowanie Wycisz %s - Dodaj zakłądkę + Dodaj zakładkę Otwórz konto osoby podbijającej Pokaż podbicia Otwórz media #%d @@ -387,35 +387,34 @@ %s głosów %s głosów - Zostało %s kończy się %s zakończone Głosuj Głosowanie w którym brałeś(-aś) udział zakończyła się Ankieta, którą stworzyłeś(aś), zakończyła się - %d dzień - - - + Zostało %d dzień + Zostało %d dni + Zostało %d dni + Zostało %d dni - %d godzina - %d godziny - %d godzin - %d godzin + Zostało %d godzina + Zostało %d godziny + Zostało %d godzin + Zostało %d godzin - %d minuta - - - + Zostało %d minuta + Zostało %d minuty + Zostało %d minut + Zostało %d minut - %d sekunda - %d sekund - %d sekund - %d sekund + Zostało %d sekunda + Zostało %d sekund + Zostało %d sekund + Zostało %d sekund Kontynuuj Wstecz diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index d7969861..c5e9a7aa 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -67,7 +67,7 @@ Mostrar boosts Denunciar Excluir - TOOT + TOOT! TOOT! Tentar novamente Fechar @@ -180,7 +180,7 @@ Porta do proxy HTTP Privacidade padrão dos toots Sempre marcar mídia como sensível - Publicação (sincronizada com o servidor) + Toots (sincronizado com instância) Erro ao sincronizar configurações Público Não-listado @@ -369,27 +369,26 @@ %s voto %s votos - %s restante termina em %s Terminou Votar Uma enquete que você votou terminou Sua enquete terminou - %d dia - %d dias + %d dia restante + %d dias restante - %d hora - %d horas + %d hora restante + %d horas restante - %d minuto - %d minutos + %d minuto restante + %d minutos restante - %d segundo - %d segundos + %d segundo restante + %d segundos restante Reproduzir GIFs Enquete com as opções: %1$s, %2$s, %3$s, %4$s; %5$s diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 67c4005b..d46030c5 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -410,7 +410,6 @@ %s голосов %s голосов - %s завершится %s завершён Голосовать diff --git a/app/src/main/res/values-sa/strings.xml b/app/src/main/res/values-sa/strings.xml new file mode 100644 index 00000000..a322838e --- /dev/null +++ b/app/src/main/res/values-sa/strings.xml @@ -0,0 +1,465 @@ + + + स्थानीयाः + सूचनाः + गृहम् + दौत्यप्रेषणं विफलं जातम् । + उपारोपणं विफलं जातम् । + चलचित्राणि चित्राणि चोभे एव नैकस्मिन्नेव प्रकटने संस्थापिते भवितुमर्हतः । + श्रव्यदृश्यसामग्र्यः रक्षयितुमनुमतिर्दातव्या । + भृशं दीर्घतमा स्थितिरियम् ! + श्रव्यदृश्यसामग्र्यः द्रष्टुमनुमतिर्दातव्या । + सा सञ्चिका नोद्घाट्यते । + नैतादृशा सञ्चिका उपारोपणीया । + श्रव्यसञ्चिका ४०MBतोऽल्पा स्थाप्या । + चलचित्रसञ्चिका ४०MBतोऽल्पा स्थाप्या । + ८ MBतोऽल्पा परिमिता सञ्चिका स्थाप्या । + सम्प्रवेशस्तोकं न लब्धः । + प्रमाणीकरणं निषिद्धम् । + अज्ञातः प्रमाणीकरणदोषो जातः । + प्रयोजनार्थं जालसञ्चारकं न लब्धम् । + तेन विशिष्टस्थलेन प्रमाणीकरणं विफलं जातम् । + अवैधानिकप्रदेशः प्रविष्टः + नैतद्रिक्तं भवितुमर्हति । + दोषो जातः । कृपया भवतोऽन्तर्जालीयसम्पर्कं परीक्ष्य पुनश्च यत्यताम् ! + दोषो जातः । + न किमप्यत्र । नवीकरणार्थमाकृष्यतामधः ! + न किमप्यत्र । + संनिपत्यताम् + विस्तार्यताम् + स्वल्पं दृश्यताम् + अधिकं दृश्यताम् + द्रष्टुमत्र नुद्यताम् + प्रच्छन्नसामग्र्यः + संवेदनशीलो विषयः + %s अप्रकाशयत् + \@%s + अनुज्ञापत्राणि + कालबद्धदौत्यानि + लेखविकर्षाः + स्वीयव्यक्तिविवरणं सम्पाद्यताम् + अनुसरणार्थमनुरोधाः + प्रच्छन्नप्रदेशाः + निषिद्धोपभोक्तारः + मूकोपभोक्तारः + पुटचिह्नानि + प्रियाः + अनुसर्तारः + अनुसरति + कीलिताः + सप्रत्युत्तरम् + प्रकटनानि + दौत्यम् + पीठिकाः + प्रत्यक्षसन्देशाः + सङ्घीयाः + %s त्वामन्वसरत् + %s भवदीयदौत्याय रुचिमददात् + %s भवदीयं दौत्यं प्राकाशयत् + प्रेष्यताम् ! + प्रेष्यताम् + विनश्य पुनश्च लिख्यताम् + नश्यताम् + सम्पाद्यताम् + आवेद्यताम् + प्रकाशनानि दृश्यन्ताम् + प्रकाशनानि छाद्यन्ताम् + अवरोधो नश्यताम् + अवरुध्यताम् + अनुसरणं नश्यताम् + अनुस्रियताम् + नूनमेव बहिर्गन्तुमीहते %1$s इति व्यक्तित्वलेखात् \? + बहिर्गम्यताम् + मास्टुडोनमाध्यमेन सम्प्रविश्यताम् + रच्यताम् + अधिकम् + प्रियता निष्क्रियताम् + पुटचिह्नं क्रियताम् + प्रियम् + प्रकाशनं निष्क्रियताम् + प्रकाश्यताम् + प्रत्युत्तरं दीयताम् + त्वरितप्रत्युत्तरं दीयताम् + अन्यटिप्पण्यः \? + आवेद्यताम् @%s + %s तवाऽनुसरणार्थंं न्यवेदयत् + सामग्र्यः युज्यन्ताम् + जालसञ्चारके उद्घाट्यताम् + सामग्र्यः + अनुसरणार्थमनुरोधाः + प्रच्छन्नप्रदेशाः + निषिद्धभोक्तारः + मूकभोक्तारः + पुटचिह्नानि + प्रियाः + लेखाविन्यासाः + विन्यासाः + व्यक्तिविवरणम् + पिधीयताम् + पुनः यत्यताम् + %s मित्रविषये मा सूच्यताम् + %s मित्रविषये सूच्यताम् + %s सशब्दं क्रियताम् + सशब्दम् + निःशब्दम् + विभाज्यताम् + चित्रं गृह्यताम् + मतदानं युज्यताम् + सामग्र्यस्मै विभाज्यताम् … + दौत्यमस्मै विभाज्यताम् … + दौत्यजालस्थलमस्मै विभाज्यताम् … + सामग्री अवारोप्यमाणा + सामग्री अवारोप्यताम् + एवं विभाज्यताम् … + %s एवमुद्घाट्यताम् + जालस्थलं प्रतिलिख्यताम् + अवारोप्यमाणम् %1$s + उद्घाट्यताम् #%d + जालस्थलानि + उल्लेखाः + प्रचलितवस्तूनि + प्रियाणि दृश्यन्ताम् + प्रकाशनानि दृश्यन्ताम् + प्रकाशनलेखकः उद्घाट्यताम् + प्रचलितवस्तूनि + उल्लेखाः + जालस्थलानि + पीठिका युज्यताम् + पुनरारम्भः + कालबद्धदौत्यं क्रियताम् + भावचिह्नटङ्कणफलकम् + विषयप्रत्यादेशः + दौत्यसुदर्शता + कालबद्धदौत्यानि + लेखविकर्षाः + अन्विष्यताम् + अस्वीक्रियताम् + स्वीक्रियताम् + अपाक्रियताम् + सम्पाद्यताम् + व्यक्तिविवरणं सम्पाद्यताम् + रक्ष्यताम् + पेटिकोद्घट्यताम् + सामग्र्यः वार्यन्ताम् + उल्लिख्यताम् + सशब्द आलापः क्रियताम् + तूष्णीमालापः क्रियताम् + %s सशब्दं क्रियताम् + %s निःशब्दं क्रियताम् + भोक्ता सशब्दः कृतः + निरवरोधः कृतः + प्रेषितम्! + सम्पर्कः क्रियते… + किं नाम विशिष्टस्थलम्\? + शीर्षः + अवतारः + प्रत्युत्तरम् … + न परिणामाः + अन्विष्यताम्… + विवरणम् + नाम + विषयप्रत्यादेशः + किं वर्तमानमस्ति \? + किं विशिष्टस्थलम् \? + सफलं प्रत्युत्तरप्रेषणम् । + प्रेषितम्! + %s अनावृतः + सूच्यतां मे यदा + ज्योत्या सूच्यताम् + कम्पनेन सूच्यताम् + ध्वनिना सूच्यताम् + सतर्कताः + सूचनाः + सूचनाः + प्रत्यक्षम् - केवलमुल्लिखितयोक्तृभ्यः प्रकट्यताम् + केवलमनुसर्तृृणाम् :- कृते प्रकट्यताम् + अनिर्दिष्टः = सार्वजनिकसमयतालिकायां मा प्रकट्यताम् + सार्वजनिकः‍‍‍‍= प्रकट्यतां सार्वजनिकसमयतालिकासु + सूचनाः छाद्यन्ताम् + निःशब्दं क्रियताम् @%s\? + किल अवरुध्यताम् @%s\? + प्रदेशः छाद्यताम् + निश्चियेन सर्वमेव निषिद्धं भवेदेतस्य जनस्य %s \? कोऽपि विषयो न द्रष्टुं शक्यते तत्प्रदेशात् कस्यामपि समयतालिकायामुत वा ते सूचनापेटिकायाम् । भवदनुसर्तारः तस्मात्प्रदेशान्निष्क्रियन्ते । + विनश्य पुनः लिख्यताम् \? + दौत्यमेतन्नश्यताम्\? + अनुसरणं नश्यताम् \? + अवारोप्यताम् + उपारोप्यमाणम्… + सामग्रीणामुपारोपणसिद्धिः वर्तमाना + कस्याऽपि विशिष्टस्थलस्य सङ्केतसूत्रमत्र टङ्कयितुं शक्यते mastodon.social, icosahedron.website, social.tchncs.de, तथेैवअधिकम् +\n +\nयदि युष्माकं व्यक्तिगतलेखाऽत्र न वर्तते तर्हि तस्य विशिष्टस्थलस्य नाम टङ्कयित्वा तत्र निर्मातुं शक्नुथ । +\n +\nविशिष्टस्थलमित्युक्ते स्थलमेकं यत्र युष्माकं लेखाः आश्रिताः, किन्तु साफल्येनैवाऽन्यविशिष्टस्थलीयैः सह सम्पर्कयितुं शक्यते । +\n +\nअधिकमत्र प्राप्यते joinmastodon.org. + येषामुसरणं करोषि तेष्वन्विष्यताम् + सूचिः सम्पाद्यताम् + सूचिर्नश्यताम् + पुनः सूचिनामकरणं क्रियताम् + सूचिः निर्मीयताम् + सूचिर्नष्टुमशक्या + पुनः सूचिनामकरणं कर्तुमशक्यम् + सूचिनिर्माणं कर्तुमशक्यम् + अनुसरणानुरोधो नश्यताम् \? + सूचेः समयतालिका + सूचयः + सूचयः + नवमास्टोडोनलेखा युज्यताम् + नवलेखा युज्यताम् + शोधनार्थं वाक्यांशः + यदा शब्दो वा वाक्यांशश्चिह्नरहितो भवति, तर्हि विन्यासोऽयं स्थाप्यते केवलं यदा पूर्णत्वेन शब्दसमानता वर्तते + सर्वः शब्दः + नवीक्रियताम् + नश्यताम् + शोधकं सम्पाद्यताम् + शोधकं युज्यताम् + आलापाः + सार्वजनिकतालिकाः + अधिकमारोप्यताम् + \@%s मित्रायोत्तरम् + सामग्र्यः + सर्वदा विषयसतर्कतयाऽङ्कितं दौत्यं विस्तार्यताम् + सर्वदा संवेदनशीलविषयो दृश्यताम् + त्वामनुसरति + %ds क्ष + %dm नि + %dh घ + %dy वर्ष + %dd दि + %ds क्ष + %dm नि + %dh घ + %dd दि + %dy वर्ष + अनुसरणं निवेदितम् + चलचित्राणि + चित्राणि + दौत्याय जालस्थानं विभाज्यताम् + दौत्यविषयो विभाज्यताम् + टस्कीवर्यस्य व्यक्तिगतविवरणम् + अशुद्धीनामावेदनं वैशिष्ट्यनिवेदनञ्च +\n https://github.com/tuskyapp/Tusky/issues + प्रकल्पस्य जालसूत्रम् : +\n https://tusky.app + टस्कीत्यनावृतस्रोतो निःशुल्कतन्त्रांशः। GNU General Public License Version 3 इत्यनेनाऽनुज्ञापितः। अत्राऽनुज्ञापत्रं द्रष्टुं शक्यते:-https://www.gnu.org/licenses/gpl-3.0.en.html + टस्कीत्यनेनाऽऽश्रितः + टस्की %s + विज्ञप्तिः + कपाटितव्यक्तिविवरणलेखः + %d नवपरस्परक्रियाः + %1$s च %2$s च + %1$s, %2$s, तथैव %3$s + %1$s, %2$s, %3$s तथा च %4$d अन्येऽपि + %s मित्रेण भवन्नामोल्लिखितम् + मतदाने समाप्ते सति सूचनाः + मतदानानि + प्रीतिः इत्यङ्किते सति सूचनाः + प्रियाः + दौत्यप्रकाशने सति सूचनाः + प्रकाशनानि + अनुसरणानुरोधान्नधिकृत्य सूचनाः + अनुसरणार्थमनुरोधाः + नवोल्लेखान्नधिकृत्य सूचनाः + नवानुसर्तृृन्नधिकृत्य सूचनाः + नवानुसर्तारः + नवोल्लेखाः + स्थूलतमः + स्थूलः + मध्यमः + सूक्ष्मः + सूक्ष्मतमः + दौत्यस्य / स्थितेरक्षराकारः + केवलमनुसर्तृभ्यः + अनिर्दिष्टम् + सार्वजनिकम् + नितलम् + शिखरम् + मुख्यमार्गणस्थितिः + विन्यासं समसामयिकं कर्तुं विफलता + प्रकाशनम् (जालवितरकेण सह सामयिकम्) + श्रव्यदृश्यसामग्रीः सदा संवेदनशीलाः इत्यङ्क्यताम् + पूर्वनिविष्टप्रकटनगुह्यता + HTTPS प्रतिनिधिद्वारिका + HTTPS प्रतिनिधिजालवितारकम् + HTTP प्रतिनिधिसंयुतनं क्रियताम् + HTTP प्रतिनिधिः + प्रतिनिधिः + सामग्रीणां पूर्वोद्घाटनमवारोप्यताम् + प्रत्युत्तराणि दृश्यन्ताम् + प्रकाशनानि दृश्यन्ताम् + पीठिकाः + समयतालिका-शोधनम् + छादितसामग्रीभ्यो बहुवर्णयुतचित्रं दर्शयतु + सञ्जीवितावतारः क्रियताम् + स्वचालितयन्त्रेभ्यः सूचको दृश्यताम् + भाषा + सारणक्रमे लेखनगण्डः छाद्यताम् + क्रोमस्वीयानुकूलपीठिकाः प्रयुज्यन्ताम् + जालसञ्चारकम् + प्रणाल्याः परिकल्पना प्रयुज्यताम् + सूर्यास्तसमये स्वचालितम् + कृष्णः + ज्योतिपूर्णः + अन्धकारः + शोधकम् + समयतालिकाः + अनुप्रयोगप्रबन्धाः + स्वरूपम् + मतदानं समाप्तम् + मम प्रकटनानि प्रियाणि + मम प्रकटनानि प्रकाशितानि + अनुसरणार्थं निवेदितम् + अनुसृतम् + उल्लिखिताः + प्राक्तु भावचिह्नसमूहोऽयमवारोप्यः + प्रणाल्यां पूर्वनिविष्टम् + भावचिह्नशैली + अंशफलकेऽनुसृतम् + भवदीयं विशिष्टस्थलं %s स्वीयानुकूलभावचिह्नरहितं वर्तते + लिख्यताम् + दौत्यप्रतिलिपिस्तत्र विकर्षेसु रक्षिता + प्रेषणं निराकृतम् + प्रेष्यमाणानि + दौत्यप्रेषणे दोषः + दौत्यं प्रेष्यमाणम्… + रक्षणीयम् \? + स्वयमेवाऽनुसर्तॄणां कृतेऽनुमतिर्दातव्या + लेखा अवरुध्यताम् + नश्यताम् + शीर्षकवाक्यं लिख्यताम् + दृष्ट्यां येषां समस्याऽस्ति तेषांं कृते विवरणम् +\n(%d परिमिता न्यूनाक्षरसङ्ख्या) + शीर्षकवाक्यं लेखितुमशक्यम् + %1$s लेखया प्रकटनं क्रियते + सूच्याः लेखा नश्यताम् + सूच्यां लेखा स्थाप्यताम् + यन्त्रम् + अवारोपणे दोषः + गुगलस्य वर्तमानभावचिह्नसमूहः + मास्टोडोनस्य पूर्वनिविष्टभावचिह्नसमूहः + ब्लाबभावचिह्नानि एन्ड्रोइड४.४तः ७.१पर्यन्तं प्रसिद्धानि + तव यन्त्रस्य पूर्वस्थापितभावचिह्नसमूहः + पुनः प्रारभ्यताम् + पश्चात् + पुनश्च टस्कीप्रारम्भोऽपेक्षितो वर्तते परिवर्तनानुसरेण चलितुम् + अनुप्रयोगप्रारम्भः आवश्यकः + दौत्यमुद्घाट्यताम् + विस्तार्यन्तां नश्यन्तां वा स्थतयः + अन्वेषणं भवद्वर्तते… + कीलयतु + कीलनं नश्यताम् + अधो लिखितवार्तोपभोक्तुः व्यक्तित्वविवरणमांशिकरूपेण प्रतिबिम्बयेत् । व्यक्तित्वविवरणमुद्घाट्यतां कश्मिंश्चिदपि जालसञ्चारके । + मूलसमयो युज्यताम् + विषयः + लक्षः + दत्तं युज्यताम् + व्यक्तिगतविवरणदत्तांशः + CC-BY-SA ४.० + CC-BY ४.० + Apache Licence (अनुलिख्यताम्) इत्यनुज्ञप्तिपत्रेणाऽनुज्ञापितः + टस्कीत्यस्मिन्निम्नलिखितेभ्योऽनावृतस्रोतःप्रकल्पेभ्यो विध्यादेशाः सन्ति: + प्रकाशनंं नश्यताम् + मूलदर्शकेभ्यः प्रकाश्यताम् + %1$s मित्रमत्र प्रस्थितम्: + न लेखविकर्षास्ते सन्ति । + %1$s + उच्चैःस्थितायाः साधनशालकायाः शीर्षकं छाद्यताम् + प्रकाशनात् प्राक् पुष्टिसंवादमञ्जूषा दर्शनीया + जालस्थानप्रदर्शनं समयतालिकायां दर्शयतु + मास्टोडोने पञ्चनिमेषपरिमितो न्यूनतमः कालबद्धसमयः । + न ते कालबद्धदौत्यानि सन्ति । + सम्पाद्यताम् + मतम् %d + बहूनि मतानि + अपरं मतं युज्यताम् + ७ दिनानि + ३ दिनानि + १ दिनम् + ६ घण्टाः + १ घण्टा + ३० निमेषाः + ५ निमेषाः + मतपेटिका + सारणहावभावस्य संयुतनं पीठिकापरिवर्तनार्थं कार्यम् + सूचनाशोधकं दृश्यताम् + अन्वेषणे विफलता जाता + व्यक्तित्वविवरणलेखाः + अन्यजालवितारकादियं व्यक्तित्वविवरणलेखा । आवेदनस्य रक्षितप्रतिलिपिरपि प्रेष्यतां वा \? + आवेदनमिदं जालवितारकाय प्रेष्यते । स्वीयविवरणमस्मिन् विषयेऽधो लेखितुं शक्नोषि-: + दौत्यानि गृहीतुं विफलता + आवेदनप्रेषणे विफलता + अस्मै पुरस्क्रियताम् %s + अन्याः टिप्पण्यः + साफल्येनाऽऽवेदनं दत्तमस्य @%s + कृतम् + पूर्वम् + निरन्तरम् + + %d क्षणम् + %d क्षणे + + + %d निमेषः शेषम् + %d निमेषौ शेषम् + + + %d घण्टा शेषम् + %d घण्टे शेषम् + + + %d दिनम् शेषम् + %d दिने शेषम् + + त्वया रचितमेकं मतदानं समाप्तम् + मतदानमेकं समाप्तं यस्मिन् त्वयाऽपि स्वीयमतं दत्तम् + मतम् + पिहितम् + समापनं यावत् %s + + %s जनः + %s जनौ + + + %s मतम् + %s मते + + %1$s • %2$s + चित्राणां कृते प्रवृत्तिः %s + किं नूनमेव सर्वाः सूचनाः स्थायित्वेन मार्जनीयाः \? + लिख्यताम् + दौत्यं लिख्यताम् + स्थाप्यताम् + शुध्यताम् + मार्ज्यन्ताम् + सूचिः + सूचिरवचीयताम् + प्रचलितानि + # चिह्नं विना प्रचलितम् + प्रचलितं युज्यताम् + सूचिनाम + मतदाने मतानि- %1$s, %2$s, %3$s, %4$s; %5$s + प्रत्यक्षम् + अनुसर्तारः + अनिर्दिष्टम् + सार्वजनिकम् + पुटचिह्नं कृतम् + प्रीतिर्दत्ता + पुनर्लिखितम् + विवरणं नास्ति + विषयपूर्वसतर्कता: %s + सामग्र्यः %s + अधिकतमपीठिकासङ्ख्या %1$d भूता + %1$s, %2$s तथा %3$d अन्येऽपि + %1$s तथैव %2$s + निम्नमित्रस्य प्रीतिः + निम्नमित्रेण प्रकाशितम् + + %s प्रकाशनम् + %s प्रकाशने + + + %1$s प्रियम् + %1$s प्रिये + + \ No newline at end of file diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 9a60f25d..5f4ca650 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -342,7 +342,6 @@ %s glasovi %s glasov - še %s se konča ob %s zaprto Glasovanje @@ -366,22 +365,22 @@ Anketa, na kateri ste glasovali, se je končala Anketa, ki ste jo ustvarili, se je končala - %d dan - %d dni - %d dni - %d dni + še %d dan + še %d dni + še %d dni + še %d dni - %d ura - %d uri - %d ure - %d ur + še %d ura + še %d uri + še %d ure + še %d ur - %d minuta - %d minuti - %d minute - %d minut + še %d minuta + še %d minuti + še %d minute + še %d minut %d sekunda @@ -437,4 +436,10 @@ %s spodbudil Ključniki Zahteve za Sledenje + + %1$s Priljubljena + %1$s Priljubljeni + %1$s Priljubljene + %1$s Priljubljenih + \ No newline at end of file diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 57291c16..de4b4409 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -373,7 +373,6 @@ %s röst %s röster - %s kvar avslutas vid %s stängd Rösta @@ -383,20 +382,20 @@ En omröstning där du har röstat är avslutad En omröstning som du har skapat har avslutats - %d dag - %d dagar + %d dag kvar + %d dagar kvar - %d timme - %d timmar + %d timme kvar + %d timmar kvar - %d minut - %d minuter + %d minut kvar + %d minuter kvar - %d sekund - %d sekunder + %d sekund kvar + %d sekunder kvar Åtgärder för bild %s Animera profil gifar @@ -477,4 +476,8 @@ Visa färgglada gradienter för gömd media Ta bort tystad %s Ta bort tystad %s + Dölj titeln i övre verktygsfältet + Dölj aviseringar + Tysta aviseringar från %s + Aktivera aviseringar från %s \ No newline at end of file diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index f53ba26e..2a5a035e 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -278,4 +278,5 @@ பின்பற்றுபவர்கள் பட்டியலிடப்படாதவர்களுக்கு அனைவருக்கும் + எழுது \ No newline at end of file diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index a14317c8..ea831d51 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -24,23 +24,22 @@ ย้อนกลับ ต่อไป - %d วินาที + เหลืออีก %d วินาที - %d นาที + เหลืออีก %d นาที - %d ชั่วโมง + เหลืออีก %d ชั่วโมง - %d วัน + เหลืออีก %d วัน โพลที่คุณสร้างสิ้นสุดลงแล้ว โพลที่คุณโหวตสิ้นสุดลงแล้ว โหวต สิ้นสุดแล้ว จบที่ %s - เหลืออีก %s %s คน @@ -364,28 +363,28 @@ คั่นหน้า ชื่นชอบ ลบบูสต์ - บูสต์ + ดัน ตอบกลับ ตอบกลับด่วน ความคิดเห็นเพิ่มเติม\? รายงาน @%s %s ต้องการติดตามคุณ %s ได้ติดตามคุณ - %s ได้ชื่นชอบ Toot คุณ - %s ได้บูสต์ Toot คุณ - ไม่อะไรเลย ลากลงเพื่อรีเฟรช! + %s ได้ชื่นชอบโพสต์ของคุณ + %s ได้ดันโพสต์ของคุณ + ไม่มีอะไรเลย ลากลงเพื่อรีเฟรช! ไม่มีอะไร ย่อ ขยาย แสดงน้อยลง แสดงเพิ่มเติม แตะเพื่อดู - สื่อที่ซ่อนไว้ + ซ่อนสื่ออยู่ เนื้อหาอ่อนไหว - %s ได้บูสต์ + %s ได้ดัน \@%s สัญญาอนุญาต - Toot แบบกำหนดเวลา + โพสต์แบบกำหนดเวลา แก้ไขโปรไฟล์ คำขอติดตาม โดเมนที่ซ่อนไว้ @@ -399,15 +398,15 @@ โพสต์ เธรด แท็บ - ข้อความแบบไดเร็กต์ - สหพันธ์ - ท้องถิ่น + ข้อความโดยตรง + ที่ติดต่อกับภายนอก + ในเซิร์ฟเวอร์ แจ้งเตือน หน้าหลัก - การส่ง Toot เกิดความผิดพลาด + การส่งโพสต์เกิดความผิดพลาด อัปโหลดล้มเหลว ไม่สามารถแนบรูปภาพและวิดีทัศน์ในโพสต์เดียวกันได้ - ต้องมีสิทธิ์เขียนบนสื่อ + ต้องมีสิทธิ์จัดเก็บสื่อ ต้องมีสิทธิ์อ่านสื่อ ไม่สามารถเปิดไฟล์ได้ ไม่สามารถอัปโหลดไฟล์ประเภทนี้ได้ @@ -434,7 +433,7 @@ ออกจากระบบ ฉบับร่าง ชื่นชอบ - การยืนยันตัวตนทางอิเล็กทรอนิกส์กับ Instance นั้นล้มเหลว + การยืนยันตัวตนกับเซิร์ฟเวอร์นั้นล้มเหลว Instance คือ\? เข้าสู่ระบบด้วย Mastodon เลือกได้หลายตัวเลือก @@ -455,4 +454,5 @@ เลิกปิดเสียงการแจ้งเตือนจาก %s ซ่อนการแจ้งเตือน ปิดเสียงการแจ้งเตือนจาก %s + ซ่อนหัวข้อของแถบเครื่องมือด้านบน \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 7f06d795..fdf123a1 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -6,17 +6,17 @@ Girilen alan alanı geçersiz Kimlik doğrulama başarısız oldu. Kullanılabilir web tarayıcısı bulunamadı. - Açıklanmayan kimlik doğrulama hata oluştu. - Kimlik doğrulama reddedildi. + Tanımlanamayan bir yetkilendirme hatası oluştu. + Yetkilendirme reddedildi. Giriş belirteci alınırken hata oluştu. Durum çok uzun! - Dosya 8 MB\'dan küçük olmalıdır. - Video dosyaları 40 MB’dan küçük olmalıdır. + Dosya 8 MB\'dan küçük olmalı. + Video dosyaları 40 MB’dan küçük olmalı. Bu biçimdeki dosyalar yüklenmez. Dosya açılamadı. - Medyayı okumak için izin gerekiyor. - Medya kaydetme izni gerekiyor. - Aynı iletiye hem video hem resim eklenemez. + Medya okuma izni gerekli. + Medya kaydetme izni gerekli. + Görüntüler ve videolar aynı duruma eklenemez. Yükleme başarısız oldu. Toot gönderilirken hata oluştu. Ana sayfa @@ -25,7 +25,7 @@ Birleşmiş Direkt Mesajlar Sekmeler - Dizi + Toot Gönderiler Yanıtlar ile Sabitlenmiş @@ -49,8 +49,8 @@ Daralt Burada hiçbir şey yok. Henüz hiç ileti yoktur. Yenilemek için aşağıya çek! - %s iletini yükseltti - %s durumunu favorilerine ekledi + %s tootunuzu boost etti + %s tootunuzu favorilerine ekledi %s seni takip etti \@%s bildir Daha fazla yorum? @@ -61,7 +61,7 @@ Daha fazla Oluştur Mastodon ile giriş yap - Çıkış Yap + Oturumu Kapat Bu %1$s oturumu sonlandırmak istediğinizden emin misiniz\? Takip et Takibi bırak @@ -116,7 +116,7 @@ Kullanıcının sesi açıldı İletildi! Yanıt başarıyla gönderildi. - Sunucu giriniz + Hangi örnek\? Neler oluyor? İçerik uyarı Görünen ad @@ -198,7 +198,7 @@ Favoriler Tootların favori olarak işaretlendiğinde %s senden bahsetti - %1$s, %2$s, %3$s ve %4$d daha + %1$s, %2$s, %3$s ve %4$d diğer %1$s, %2$s ve %3$s %1$s ve %2$s %d yeni etkileşim @@ -250,13 +250,13 @@ Hesabı Kilitle Takipçileri elle onaylamanız gerekir Taslaklara kaydedilsin mi\? - Durum gönderiliyor… - Durum gönderilirken hata oluştu - Toot gönderiliyor + Toot gönderiliyor… + Toot gönderilirken hata oluştu + Toot Gönderiliyor Gönderme iptal edildi Tootun bir kopyası taslaklara kaydedildi Oluştur - %s sunucunuzun herhangi bir özel ifadeye sahip değil + %s örneğinizin herhangi bir özel ifadesi yok Panoya kopyalandı İfade stili Sistem varsayılanı @@ -327,7 +327,7 @@ Kaldır Güncelle Tüm dünya - Anahtar kelime veya kelime öbeği yalnızca alfanümerik olduğunda, yalnızca tüm kelimeyle eşleşirse uygulanır. + Bir anahtar kelime veya kelime öbeği sadece alfanümerik olduğunda, yalnızca tüm kelimeyle eşleşirse uygulanır Filtrelenecek ifade Liste oluşturulamadı Liste yeniden adlandırılamadı @@ -361,29 +361,28 @@ %s görüntüsü için eylemler %1$s • %2$s - - + %s oy + %s oy - %s kaldı kapandı Oy Oy verdiğin bir anket sona erdi Oluşturduğun bir anket sona erdi - - + %d gün kaldı + %d gün kaldı - %d saat - %d saat + %d saat kaldı + %d saat kaldı - - + %d dakika kaldı + %d dakika kaldı - %d saniye - %d saniye + %d saniye kaldı + %d saniye kaldı Devam Geri @@ -402,7 +401,7 @@ #%d medyayı aç Yer imleri Zamanlanmış iletiler - Yerimi + Yer imi Düzenle Sil ve düzenle Yer imleri @@ -433,8 +432,8 @@ Alt Metin %s alan adını gizleme Alan adından her şeyi gizle - Hassas içerikleri göster - %s sona eriyor + Her zaman içerik uyarılarıyla işaretlenmiş alanları genişlet + %s içinde sona erecek Bildirilemedi Seçenek %d %s gönderisi aranırken hata oluştu @@ -446,8 +445,8 @@ Bağlantı önizlemelerini zaman çizelgesinde göster Sekmeler arasında geçiş yapmak için kaydırma hareketini etkinleştir - - + %s kişi + %s kişi Hashtag ekle Takip istekleri ile ilgili bildirimler @@ -462,12 +461,13 @@ %s kullanıcısından gelen bildirimleri yoksay %s sesini aç %s seni takip etmek istiyor - Ses dosyaları 40 MB\'dan büyük olamaz. + Ses dosyaları 40 MB\'dan küçük olmalı. Sohbetin sesini aç takip istendi Sohbeti sessize al Ana gezinti konumu Gizli medya için renkli gradyanlar göster - Başlık ayarlama başarısız oldu + Başlık ayarlanamadı Mastodon\'un minimum 5 dakikalık zamanlama aralığı vardır. + Üst araç çubuğunun başlığını gizle \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index d819665f..cb7869ad 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -45,7 +45,7 @@ Пошук… Про себе Що відбувається\? - Надіслано! + Надіслати! Надіслано! Поділитися як … Відкрити як %s @@ -116,4 +116,35 @@ Медіа Сповіщення Сповіщення + Заблоковані користувачі + Вподобане + Запити на підписку + Розблокувати %s + Відмінити приглушення + Приховані домени + Список глушіння + ТООТ! + ТООТ + Показати просування + Приховати просування + Розгорнути + Забртаи просунення + Просунути + %s сподабався ваш статус + %s просунув(ла) ваш статус + Згорнути + Розгорнути + Чутливий вміст + %s Просунув(ла) + Приховані домени + Заглушені користувачі + Дописи + Поширити + Вкладки + Завантаження не вдалося. + Не вдалося отримати токін авторизації. + Авторизація була відхилина. + Сталася помилка неопізнаної авторизації. + Помилка входу з цією інстанцією. + Введено недійсний домен \ No newline at end of file diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 1f99b5ac..7e97c1f4 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -48,7 +48,7 @@ Làm tươi Tìm kiếm Trang cá nhân - Tài khoản + Riêng bạn Cài đặt Đăng xuất Xong @@ -67,11 +67,11 @@ Đang tải… Đã tải xong tập tin Bạn phải nhập một tên miền, ví dụ mastodon.social, icosahedron.website, social.tchncs.de, và nhiều hơn nữa! -\n -\nNếu chưa có tài khoản, bạn phải tạo tài khoản trước ở đó. -\n -\nMột máy chủ là một cộng đồng nơi mà tài khoản của bạn lưu trữ trên đó, nhưng bạn cũng có thể giao tiếp và theo dõi với mọi người trên các máy chủ khác một cách dễ dàng. -\n +\n +\nNếu chưa có tài khoản, bạn phải tạo tài khoản trước ở đó. +\n +\nMáy chủ, nói cách khác là một cộng đồng nơi mà tài khoản của bạn lưu trữ trên đó, nhưng bạn vẫn có thể giao tiếp và theo dõi mọi người trên các máy chủ khác một cách dễ dàng. +\n \nTham khảo thêm tại joinmastodon.org. Đang kết nối… Ảnh bìa @@ -82,7 +82,7 @@ Tiểu sử Tên hiển thị Nội dung nhạy cảm - Chuyện gì đang xảy ra\? + Bạn đang nghĩ về điều gì\? Bạn ở máy chủ nào\? Trả lời đã được gửi đi. Đã đăng! @@ -178,7 +178,7 @@ %s chia sẻ \@%s Giấy phép - Tút đã lên lịch + Lịch đăng Chỉnh sửa trang cá nhân Yêu cầu theo dõi Máy chủ đã ẩn @@ -191,7 +191,7 @@ Tương tác Tút Tút - Xếp Tab + Xếp tab Tin nhắn Thế giới Cộng đồng @@ -207,12 +207,12 @@ Lọc bảng tin Che mờ nội dung nhạy cảm Hiện ảnh đại diện GIF - Hiện tút từ tài khoản Bot + Hiện icon cho tài khoản Bot Ngôn ngữ Ẩn nút viết tút khi xem bảng tin - Sử dụng tab Chrome + Mở luôn trong app Trình duyệt - Sử dụng mặc định của thiết bị + Mặc định của thiết bị Tự động khi trời tối Đen Sáng @@ -259,7 +259,7 @@ Trên màn hình Vị trí menu Đồng bộ hoá thất bại - Đăng (đồng bộ với server) + Đăng (đồng bộ với máy chủ) Luôn đánh dấu nội dung là nhạy cảm Trạng thái tút mặc định HTTP proxy server @@ -298,7 +298,7 @@ %d giờ %d ngày %d năm - in %ds + %ds %d phút in %d giờ in %d ngày @@ -306,8 +306,8 @@ Yêu cầu theo dõi Video Hình ảnh - Chia sẻ URL tút - Chia sẻ nội dung tút + URL tút + Nội dung của tút Trang cá nhân Tusky Hiện xác nhận trước khi chia sẻ Hiện xem trước của link @@ -326,35 +326,34 @@ 30 phút 5 phút Bình chọn - Sử dụng thao tác cử chỉ để chuyển qua lại giữa các tab + Vuốt để chuyển qua lại giữa các tab Hiện bộ lọc thông báo Không thể tìm thấy Người - Tài khoản này thuộc máy chủ khác. Gửi báo cáo ẩn danh\? - Báo cáo này sẽ được gửi tới kiểm duyệt viên máy chủ của bạn. Hãy cung cấp nội dung vì sao bạn báo cáo người này bên dưới: + Tài khoản này thuộc máy chủ khác. Gửi luôn cho máy chủ đó\? + Báo cáo này sẽ được gửi tới kiểm duyệt viên. Hãy cho biết lý do vì sao bạn báo cáo người này bên dưới: Không tải được tút Báo cáo thất bại - Dời sang %s + Gửi cho %s Thêm ghi chú Đã gửi báo cáo @%s - %d giây + %d giây nữa kết thúc - %d phút + %d phút nữa kết thúc - %d giờ + %d giờ nữa kết thúc - %d ngày + %d ngày nữa kết thúc Cuộc bình chọn bạn tạo đã kết thúc - Một cuộc bình chọn mà bạn tham gia đã kết thúc + Cuộc bình chọn bạn tham gia đã kết thúc Bình chọn Kết thúc kết thúc lúc %s - %s nữa kết thúc %s người @@ -371,7 +370,7 @@ Danh sách Chọn danh sách Hashtag - Hashtag mà không # + Không cần dấu # Thêm hashtag Tên danh sách Những lựa chọn: %1$s, %2$s, %3$s, %4$s; %5$s @@ -409,7 +408,7 @@ Licensed under the Apache License (sao chép bên dưới) Tusky có sử dụng mã nguồn từ những dự án mã nguồn mở sau: Hủy chia sẻ - Chia sẻ với người đăng + Chia sẻ công khai %1$s đã dời sang: Tài khoản Bot Tải về thất bại @@ -448,11 +447,16 @@ Thêm tài khoản Mastodon Thêm tài khoản Thêm mô tả - Khi từ khóa là chữ và số, nó sẽ chỉ được áp dụng nếu nó phù hợp với toàn bộ từ + Bất kể từ khóa là từ hoặc cụm từ, những kết quả hiện ra sẽ giống hệt như bạn nhập Media: %s Ẩn thông báo Ẩn thông báo từ %s Bỏ ẩn thông báo từ %s Bỏ ẩn %s Bỏ ẩn %s + Ẩn tiêu đề tab + Đã lưu! + Chú thích của bạn về người này + Chưa có thông báo. + Thông báo \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 7698d7d9..8ce9eb19 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -1,24 +1,24 @@ - 应用程序出现异常。 + 应用程序出现异常 网络请求出错,请检查互联网连接并重试! - 内容不能为空。 + 内容不能为空 该域名无效 - 无法连接此服务器。 - 没有可用的浏览器。 - 认证过程出现未知错误。 - 授权被拒绝。 - 无法获取登录信息。 + 无法连接此服务器 + 没有可用的浏览器 + 认证过程出现未知错误 + 授权被拒绝 + 无法获取登录信息 嘟文太长了! - 文件大小限制 8MB。 - 视频文件大小限制 40MB。 - 无法上传此类型的文件。 - 此文件无法打开。 - 需要授予 Tusky 读取媒体文件的权限。 - 需要授予 Tusky 写入存储空间的权限。 - 无法在嘟文中同时插入视频和图片。 - 媒体文件上传失败。 - 嘟文发送时出错。 + 文件大小限制 8MB + 视频文件大小限制 40MB + 无法上传此类型的文件 + 此文件无法打开 + 需要授予 Tusky 读取媒体文件的权限 + 需要授予 Tusky 写入存储空间的权限 + 无法在嘟文中同时插入视频和图片 + 媒体文件上传失败 + 嘟文发送时出错 主页 通知 本站时间轴 @@ -47,7 +47,7 @@ 折叠内容 展开 折叠 - 还没有内容。 + 还没有内容 还没有内容,向下拉动即可刷新! %s 转嘟了你的嘟文 %s 收藏了你的嘟文 @@ -81,20 +81,20 @@ 个人资料 设置 帐户设置 - 我的收藏 + 收藏 被隐藏的用户 被屏蔽的用户 关注请求 媒体 在浏览器中打开 - 从相册中选择 + 添加媒体 拍照 分享 隐藏 取消隐藏 提及 隐藏媒体文件 - 打开应用抽屉 + 打开菜单 保存 编辑个人资料 编辑 @@ -130,7 +130,7 @@ 已解除屏蔽 已取消隐藏 已发送! - 成功发送回复。 + 成功发送回复 域名 有什么新鲜事? 内容提醒 @@ -164,7 +164,7 @@ 通知 通知 提醒 - 通知铃声 + 铃声 振动 呼吸灯 事件 @@ -177,9 +177,9 @@ 应用主题 时间轴 过滤器 - 黑夜 - 白天 - 暗色 + 暗色 + 亮色 + 黑色 自动切换 跟随系统设定 浏览器 @@ -215,13 +215,13 @@ 当有用户关注我时 转嘟 当有用户转嘟了我的嘟文时 - 我的收藏 + 收藏 当有用户收藏了我的嘟文时 投票 当我参与的投票结束时 %s 提及了你 - %1$s, %2$s, %3$s 和 %4$d 人 - %1$s, %2$s, 和 %3$s + %1$s,%2$s,%3$s 和 %4$d 等人 + %1$s,%2$s 和 %3$s %1$s 和 %2$s %d 个新互动 锁嘟用户 @@ -245,7 +245,7 @@ Tusky 官方帐号 分享嘟文内容 分享嘟文链接 - 照片 + 图片 视频 已发送关注请求 @@ -288,7 +288,8 @@ 从列表中移除用户 以 %1$s 发布嘟文 设置图片标题失败 - 为视觉障碍用户提供的描述\n(限制 %d 字) + 为视觉障碍用户提供的描述 +\n(限制 %d 字) 设置图片标题 移除 保护你的帐户(锁嘟) @@ -297,9 +298,9 @@ 正在发送… 发送失败 嘟文发送中 - 发送已被取消 + 已取消发送 嘟文已保存为草稿 - 发表新嘟文 + 发表嘟文 当前实例 %s 没有自定义表情符号 已复制到剪贴板 表情符号风格 @@ -343,14 +344,10 @@ 收藏 %1$s %1$s 和 %2$s - %1$s, %2$s 和 %3$d 等人 + %1$s,%2$s 和 %3$d 等人 标签页不能超过 %1$d 个 - - 媒体: %s - - - 内容提醒: %s - + 媒体:%s + 内容提醒:%s 没有媒体描述信息 @@ -366,7 +363,7 @@ 不公开 - 关注者 + 仅关注者 私信 @@ -375,17 +372,14 @@ 清空 分类 应用 - 发表新嘟文 - 发表新嘟文 + 发表嘟文 + 发表嘟文 显示机器人标志 你确定要永久清空通知列表吗? - - - %1$s • %2$s + %1$s • %2$s %s 次投票 - 剩余 %s %s 结束 已结束 投票 @@ -393,41 +387,41 @@ 你创建的投票已结束 - %d 天 + 剩余 %d 天 - %d 小时 + 剩余 %d 小时 - %d 分钟 + 剩余 %d 分钟 - %d 秒 + 剩余 %d 秒 重置 - 音频文件大小必须小于40M。 + 音频文件大小限制 40M 书签 - 隐藏域名 + 隐藏的域名 定时嘟文 书签 编辑 书签 - 隐藏域名 - 新增意见调查 + 隐藏的域名 + 新增投票 定时嘟文 - 预订嘟文 - %s 已解除静音 - 隐藏整个域 + 定时嘟文 + %s 已取消隐藏 + 隐藏来自该域名的所有嘟文 动画GIF头像 由Tusky提供支持 始终扩展标有内容警告的嘟文 整个单词 如果关键字或缩写只有字母或数字,则只有在匹配整个单词时才会应用 - Google正在使用的Emoji符号集 - 已收藏 - 使用以下选项创建投票:%1$s, %2$s, %3$s, %4$s; %5$s - 选择清单 - 清单 + Google正在使用的表情符号 + 被加入书签 + 使用以下选项创建投票:%1$s,%2$s,%3$s,%4$s;%5$s + 选择列表 + 列表 继续 返回 完成 @@ -439,7 +433,7 @@ 该报告将发送给给您的服务器管理员。您可以在下面提供有关回报此帐户的原因的说明: 该帐户来自其他服务器。向那里发送一份匿名的报告副本? 账户 - 搜寻失败 + 搜索失败 显示通知过滤器 投票 5 分钟 @@ -450,10 +444,10 @@ 3 天 7 天 添加选择 - 多个选择 + 多项选择 选择 %d 编辑 - 查找帖子时出错 %s + 查找嘟文时出错 %s 您没有草稿。 您没有任何定时嘟文。 Mastodon的最小预订时间为5分钟。 @@ -473,14 +467,18 @@ 确定隐藏 @%s? 确定屏蔽 @%s? 确定要完全屏蔽 %s 吗?您将不能在公共时间轴和通知内看见来自此域名的内容,且您在此域名上的关注者将会被移除。 - 取消隐藏本会话的通知 - 隐藏本会话的通知 + 取消隐藏会话 + 隐藏会话 取消隐藏 %s 隐藏 %s 隐藏来自 %s 的通知 取消隐藏来自 %s 的通知 取消隐藏 %s %s 请求关注你 - 主导航位置 - 隐藏顶部工具栏的标题 + 导航栏位置 + 隐藏顶部工具栏标题 + + %s 人 + + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index ad226a11..91bed1d4 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -20,7 +20,7 @@ 媒體檔案上傳失敗 嘟文發送時出錯 主頁 - 通知 + 通知設定 本站時間軸 跨站公開時間軸 私信 @@ -30,7 +30,7 @@ 嘟文和回覆 已置頂 正在關注 - 僅關注者 + 關注者 我的收藏 被靜音的使用者 被封鎖的使用者 @@ -61,7 +61,7 @@ 收藏 取消收藏 更多 - 新嘟文 + 撰寫嘟文 登入 Mastodon 帳號 登出 確定要登出 %1$s 嗎? @@ -133,7 +133,7 @@ 成功送出回覆 域名 有什麼新鮮事? - 設定敏感內容警告 + 敏感內容警告 暱稱 簡介 搜尋… @@ -143,7 +143,7 @@ 標題 什麼是站點? 正在連線… - "請輸入你帳號所在的 Mastodon 站點的域名或地址 " + 請輸入你帳號所在的 Mastodon 站點的域名或地址 正在完成上傳… 正在上傳… 下載 @@ -155,7 +155,7 @@ 不公開:所有人可見,但不會出現在公開時間軸上 僅關注者:只有經過你確認後關注你的使用者可見 私信:只有被提及的使用者可見 - 通知設定 + 通知 通知 提醒 通知鈴聲 @@ -292,7 +292,7 @@ 嘟文發送中 發送已被取消 嘟文已儲存為草稿 - 撰寫嘟文 + 新嘟文 當前站點 %s 沒有自訂表情符號 已複製到剪貼簿 表情符號風格 @@ -380,7 +380,6 @@ %s 次投票 - 剩餘 %s %s 結束 已結束 投票 @@ -388,18 +387,19 @@ 你創建的投票已結束 - %d 天 + 剩餘 %d 天 - %d 小時 + 剩餘 %d 小時 - %d 分鐘 + 剩餘 %d 分鐘 - %d 秒 + 剩餘 %d 秒 編輯 話題 關注請求 + 編輯 \ No newline at end of file diff --git a/app/src/main/res/values-zh-rMO/strings.xml b/app/src/main/res/values-zh-rMO/strings.xml index 4fb27d24..8333ff69 100644 --- a/app/src/main/res/values-zh-rMO/strings.xml +++ b/app/src/main/res/values-zh-rMO/strings.xml @@ -30,7 +30,7 @@ 嘟文和回覆 已置頂 正在關注 - 僅關注者 + 關注者 我的收藏 被靜音的使用者 被封鎖的使用者 @@ -61,7 +61,7 @@ 收藏 取消收藏 更多 - 發表新嘟文 + 新嘟文 登入 Mastodon 帳號 登出 確定要登出 %1$s 嗎? @@ -129,7 +129,7 @@ 已發送! 已解除封鎖 已解除靜音 - 已發送! + 已檢舉! 成功送出回覆 域名 有什麼新鮮事? @@ -359,7 +359,7 @@ 不公開 - 僅關注者 + 關注者 私信 @@ -369,7 +369,7 @@ 分類 應用 發表新嘟文 - 新嘟文 + 發表新嘟文 顯示機器人標誌 你確定要永久清空通知列表嗎? @@ -378,7 +378,6 @@ %s 次投票 - 剩餘 %s %s 結束 已結束 投票 @@ -386,18 +385,19 @@ 你創建的投票已結束 - %d 天 + 剩餘 %d 天 - %d 小時 + 剩餘 %d 小時 - %d 分鐘 + 剩餘 %d 分鐘 - %d 秒 + 剩餘 %d 秒 關注請求 話題 編輯 + 編輯 \ No newline at end of file diff --git a/app/src/main/res/values-zh-rSG/strings.xml b/app/src/main/res/values-zh-rSG/strings.xml index 31ff31a8..7fe476a6 100644 --- a/app/src/main/res/values-zh-rSG/strings.xml +++ b/app/src/main/res/values-zh-rSG/strings.xml @@ -20,7 +20,7 @@ 媒体文件上传失败 嘟文发送时出错 主页 - 通知设置 + 通知 本站时间轴 跨站公共时间轴 私信 @@ -159,7 +159,7 @@ 仅关注者:只有经过你确认后关注你的用户可见 私信:只有被提及的用户可见 通知设置 - 通知设置 + 通知 提醒 通知铃声 振动 @@ -382,7 +382,6 @@ %s 次投票 - 剩余 %s %s 结束 已结束 投票 @@ -390,18 +389,19 @@ 你创建的投票已结束 - %d 天 + 剩余 %d 天 - %d 小时 + 剩余 %d 小时 - %d 分钟 + 剩余 %d 分钟 - %d 秒 + 剩余 %d 秒 话题 编辑 编辑 + 关注请求 \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index a6b68854..f4e93299 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -30,8 +30,8 @@ 嘟文和回覆 已置頂 正在關注 - 僅關注者 - 我的收藏 + 關注者 + 我的最愛 被靜音的使用者 被封鎖的使用者 關注請求 @@ -143,7 +143,7 @@ 標題 什麼是站點? 正在連線… - "請輸入你帳號所在的 Mastodon 站點的域名或地址 " + 請輸入你帳號所在的 Mastodon 站點的域名或地址 正在完成上傳… 正在上傳… 下載 @@ -156,7 +156,7 @@ 僅關注者:只有經過你確認後關注你的使用者可見 私信:只有被提及的使用者可見 通知設定 - 通知設定 + 通知 提醒 通知鈴聲 振動 @@ -292,7 +292,7 @@ 嘟文發送中 發送已被取消 嘟文已儲存為草稿 - 撰寫嘟文 + 發表新嘟文 當前站點 %s 沒有自訂表情符號 已複製到剪貼簿 表情符號風格 @@ -380,7 +380,6 @@ %s 次投票 - 剩餘 %s %s 結束 已結束 投票 @@ -388,16 +387,16 @@ 你創建的投票已結束 - %d 天 + 剩餘 %d 天 - %d 小時 + 剩餘 %d 小時 - %d 分鐘 + 剩餘 %d 分鐘 - %d 秒 + 剩餘 %d 秒 隱藏域名 隱藏域名 @@ -407,7 +406,7 @@ 隱藏整個域名 GIF 動畫大頭貼 Tusky %s - 整個文字 + 整個單詞 如果關鍵字或短語僅為字母或數字,則只有在匹配整個單詞時才會應用 Google 正在使用的 Emoji 符號集 使用以下選項創建投票:%1$s, %2$s, %3$s, %4$s; %5$s @@ -426,4 +425,7 @@ 話題 關注請求 編輯 + 編輯 + 書籤 + 音檔必需小於40MB。 \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index a9fe4fc9..08260627 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -6,6 +6,7 @@ #fab207 #19a341 #25d069 + #DF1553 #fff #000 diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 243b10bc..dd4f599c 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -50,4 +50,6 @@ 108dp 16dp + + 36dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 800e62dc..bc340a3d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -42,6 +42,7 @@ Edit your profile Drafts Scheduled toots + Announcements Licenses \@%s @@ -510,7 +511,6 @@ %s person %s people - %s left ends at %s closed @@ -521,20 +521,20 @@ - %d day - %d days + %d day left + %d days left - %d hour - %d hours + %d hour left + %d hours left - %d minute - %d minutes + %d minute left + %d minutes left - %d second - %d seconds + %d second left + %d seconds left Continue @@ -570,9 +570,12 @@ You don\'t have any drafts. You don\'t have any scheduled statuses. + There are no announcements. Mastodon has a minimum scheduling interval of 5 minutes. Show link previews in timelines Show confirmation dialog before boosting Hide the title of the top toolbar + Your private note about this account + Saved! diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 0c9c66f4..50da2622 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -79,6 +79,7 @@ ?attr/colorSurface + @style/Widget.MaterialComponents.Chip.Choice