Merge branch 'develop'
This commit is contained in:
commit
d75e4a4ac4
160 changed files with 3673 additions and 2335 deletions
|
@ -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"
|
||||
}
|
||||
|
|
8
app/proguard-rules.pro
vendored
8
app/proguard-rules.pro
vendored
|
@ -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 *;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
</activity>
|
||||
<activity
|
||||
android:name=".SavedTootActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"/>
|
||||
android:configChanges="orientation|screenSize|keyboardHidden" />
|
||||
<activity
|
||||
android:name=".LoginActivity"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
|
@ -105,7 +105,7 @@
|
|||
<activity
|
||||
android:name=".components.compose.ComposeActivity"
|
||||
android:theme="@style/TuskyDialogActivityTheme"
|
||||
android:windowSoftInputMode="stateVisible|adjustResize"/>
|
||||
android:windowSoftInputMode="stateVisible|adjustResize" />
|
||||
<activity
|
||||
android:name=".ViewThreadActivity"
|
||||
android:configChanges="orientation|screenSize" />
|
||||
|
@ -117,7 +117,7 @@
|
|||
android:name=".AccountActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize" />
|
||||
<activity android:name=".EditProfileActivity" />
|
||||
<activity android:name=".PreferencesActivity" />
|
||||
<activity android:name=".components.preference.PreferencesActivity" />
|
||||
<activity android:name=".StatusListActivity" />
|
||||
<activity android:name=".AccountListActivity" />
|
||||
<activity android:name=".AboutActivity" />
|
||||
|
@ -145,6 +145,7 @@
|
|||
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
||||
<activity android:name=".components.instancemute.InstanceListActivity" />
|
||||
<activity android:name=".components.scheduled.ScheduledTootActivity" />
|
||||
<activity android:name=".components.announcements.AnnouncementsActivity" />
|
||||
|
||||
<receiver android:name=".receiver.NotificationClearBroadcastReceiver" />
|
||||
<receiver
|
||||
|
@ -180,7 +181,7 @@
|
|||
android:name="androidx.work.impl.WorkManagerInitializer"
|
||||
android:authorities="${applicationId}.workmanager-init"
|
||||
android:exported="false"
|
||||
tools:node="remove"/>
|
||||
tools:node="remove" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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<Resource<Account>> { 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<Resource<Instance>> { 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<Resource<Nothing>> {
|
||||
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<Resource<Bitmap>> {
|
||||
liveData.observe(this, {
|
||||
|
||||
when (it) {
|
||||
is Success -> {
|
||||
|
|
|
@ -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<RadioButton> 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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<Drawable>(navIconSize, navIconSize) {
|
||||
|
||||
override fun onLoadStarted(placeholder: Drawable?) {
|
||||
if(placeholder != null) {
|
||||
mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize)
|
||||
}
|
||||
}
|
||||
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
|
||||
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<IProfile> = 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -164,6 +164,7 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd
|
|||
List<String> 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);
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Any>
|
||||
|
||||
@Inject
|
||||
lateinit var notificationWorkerFactory: NotificationWorkerFactory
|
||||
lateinit var notificationWorkerFactory: Lazy<NotificationWorkerFactory>
|
||||
|
||||
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) {
|
||||
|
|
|
@ -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<AttachmentViewData>? = null
|
||||
private val toolbarVisibilityListeners = mutableListOf<ToolbarVisibilityListener>()
|
||||
private var imageUrl: String? = null
|
||||
|
||||
fun addToolbarVisibilityListener(listener: ToolbarVisibilityListener): Function0<Boolean> {
|
||||
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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<NotificationViewData> 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();
|
||||
|
|
|
@ -37,18 +37,21 @@ class PollAdapter: RecyclerView.Adapter<PollViewHolder>() {
|
|||
private var votersCount: Int? = null
|
||||
private var mode = RESULT
|
||||
private var emojis: List<Emoji> = emptyList()
|
||||
private var resultClickListener: View.OnClickListener? = null
|
||||
|
||||
fun setup(
|
||||
options: List<PollOptionViewData>,
|
||||
voteCount: Int,
|
||||
votersCount: Int?,
|
||||
emojis: List<Emoji>,
|
||||
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<PollViewHolder>() {
|
|||
val level = percent * 100
|
||||
|
||||
holder.resultTextView.background.level = level
|
||||
|
||||
holder.resultTextView.setOnClickListener(resultClickListener)
|
||||
}
|
||||
SINGLE -> {
|
||||
val emojifiedPollOptionText = option.title.emojify(emojis, holder.radioButton)
|
||||
|
|
|
@ -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<Drawable> 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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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<TabData>,
|
||||
|
@ -62,16 +62,17 @@ class TabAdapter(private var data: List<TabData>,
|
|||
|
||||
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<TabData>,
|
|||
|
||||
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<TabData>,
|
|||
* 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<TabData>,
|
|||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return data.size
|
||||
}
|
||||
override fun getItemCount() = data.size
|
||||
|
||||
fun setRemoveButtonVisible(enabled: Boolean) {
|
||||
if (removeButtonEnabled != enabled) {
|
||||
|
|
|
@ -19,4 +19,5 @@ data class ProfileEditedEvent(val newProfileData: Account) : Dispatchable
|
|||
data class PreferenceChangedEvent(val preferenceKey: String) : Dispatchable
|
||||
data class MainTabsChangedEvent(val newTabs: List<TabData>) : Dispatchable
|
||||
data class PollVoteEvent(val statusId: String, val poll: Poll) : Dispatchable
|
||||
data class DomainMuteEvent(val instance: String): Dispatchable
|
||||
data class DomainMuteEvent(val instance: String): Dispatchable
|
||||
data class AnnouncementReadEvent(val announcementId: String): Dispatchable
|
||||
|
|
|
@ -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 <http://www.gnu.org/licenses>. */
|
||||
|
||||
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<Announcement> = emptyList(),
|
||||
private val listener: AnnouncementActionListener
|
||||
) : RecyclerView.Adapter<AnnouncementAdapter.AnnouncementViewHolder>() {
|
||||
|
||||
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<Announcement>) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses>. */
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses>. */
|
||||
|
||||
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<Resource<List<Announcement>>>()
|
||||
val announcements: LiveData<Resource<List<Announcement>>> = announcementsMutable
|
||||
|
||||
private val emojisMutable = MutableLiveData<List<Emoji>>()
|
||||
val emojis: LiveData<List<Emoji>> = emojisMutable
|
||||
|
||||
init {
|
||||
Singles.zip(
|
||||
mastodonApi.getCustomEmojis(),
|
||||
appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
|
||||
.map<Either<InstanceEntity, Instance>> { 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"
|
||||
}
|
||||
}
|
|
@ -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<ComposeOptions?>(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<String>? = null,
|
||||
|
@ -1008,7 +1016,8 @@ class ComposeActivity : BaseActivity(),
|
|||
var mediaAttachments: List<Attachment>? = 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
|
||||
|
|
|
@ -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<InstanceEntity?> = MutableLiveData(null)
|
||||
|
||||
|
@ -93,6 +96,7 @@ class ComposeViewModel
|
|||
|
||||
private val mediaToDisposable = mutableMapOf<Long, Disposable>()
|
||||
|
||||
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<Unit> {
|
||||
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<String>()
|
||||
|
@ -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<Boolean> {
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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<PagedList<ConversationEntity>> {
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -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<Notification> {
|
||||
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<Notification>()
|
||||
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"
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
|
|
|
@ -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<Notification>) {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@
|
|||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.fragment.preference
|
||||
package com.keylesspalace.tusky.components.preference
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Drawable
|
|
@ -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<RadioButton>()
|
||||
private var updated = false
|
||||
private var currentNeedsUpdate = false
|
||||
|
||||
private val downloadDisposables = MutableList<Disposable?>(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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@
|
|||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.fragment.preference
|
||||
package com.keylesspalace.tusky.components.preference
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.preference.PreferenceFragmentCompat
|
|
@ -13,7 +13,7 @@
|
|||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky
|
||||
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
|
|
@ -13,13 +13,13 @@
|
|||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.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
|
|
@ -13,7 +13,7 @@
|
|||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.fragment.preference
|
||||
package com.keylesspalace.tusky.components.preference
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.preference.PreferenceFragmentCompat
|
|
@ -13,7 +13,7 @@
|
|||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.fragment.preference
|
||||
package com.keylesspalace.tusky.components.preference
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.preference.PreferenceFragmentCompat
|
|
@ -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() {
|
||||
|
|
|
@ -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<Screen>()
|
||||
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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?) {
|
||||
|
|
|
@ -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<PagedList<Status>> {
|
||||
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?) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<T> : 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<T> : Fragment(),
|
|||
showError()
|
||||
}
|
||||
checkNoData()
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
networkState.observe(viewLifecycleOwner, Observer {
|
||||
networkState.observe(viewLifecycleOwner) {
|
||||
|
||||
progressBarBottom.visible(it == NetworkState.LOADING)
|
||||
|
||||
if (it.status == Status.FAILED) {
|
||||
showError()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkNoData() {
|
||||
|
|
|
@ -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<Pair<Status, StatusViewData.Concre
|
|||
private fun onMute(accountId: String, accountUsername: String) {
|
||||
showMuteAccountDialog(
|
||||
this.requireActivity(),
|
||||
accountUsername,
|
||||
{ notifications -> viewModel.muteAccount(accountId, notifications) }
|
||||
)
|
||||
accountUsername
|
||||
) { notifications ->
|
||||
viewModel.muteAccount(accountId, notifications)
|
||||
}
|
||||
}
|
||||
|
||||
private fun accountIsInMentions(account: AccountEntity?, mentions: Array<Mention>): Boolean {
|
||||
|
|
|
@ -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<AccountEntity> {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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)""")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 <http://www.gnu.org/licenses>. */
|
||||
|
||||
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<Status.Mention>,
|
||||
val statuses: List<Status>,
|
||||
val tags: List<HashTag>,
|
||||
val emojis: List<Emoji>,
|
||||
val reactions: List<Reaction>
|
||||
) {
|
||||
|
||||
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?
|
||||
)
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
) : Parcelable
|
||||
|
|
15
app/src/main/java/com/keylesspalace/tusky/entity/Marker.kt
Normal file
15
app/src/main/java/com/keylesspalace/tusky/entity/Marker.kt
Normal file
|
@ -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
|
||||
)
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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<Relationship> {
|
||||
override fun onResponse(call: Call<Relationship>, response: Response<Relationship>) {
|
||||
if (response.isSuccessful) {
|
||||
onMuteSuccess(mute, id, position, notifications)
|
||||
} else {
|
||||
onMuteFailure(mute, id, notifications)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<Relationship>, 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<Relationship> {
|
||||
override fun onResponse(call: Call<Relationship>, response: Response<Relationship>) {
|
||||
if (response.isSuccessful) {
|
||||
onBlockSuccess(block, id, position)
|
||||
} else {
|
||||
onBlockFailure(block, id)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<Relationship>, 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<String>) {
|
||||
val callback = object : Callback<List<Relationship>> {
|
||||
override fun onResponse(call: Call<List<Relationship>>, response: Response<List<Relationship>>) {
|
||||
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<List<Relationship>>, t: Throwable) {
|
||||
onFetchRelationshipsFailure(ids)
|
||||
}
|
||||
}
|
||||
|
||||
val call = api.relationships(ids)
|
||||
callList.add(call)
|
||||
call.enqueue(callback)
|
||||
}
|
||||
|
||||
private fun onFetchRelationshipsSuccess(relationships: List<Relationship>) {
|
||||
val mutesAdapter = adapter as MutesAdapter
|
||||
var mutingNotificationsMap = HashMap<String, Boolean>()
|
||||
val mutingNotificationsMap = HashMap<String, Boolean>()
|
||||
relationships.map { mutingNotificationsMap.put(it.id, it.mutingNotifications) }
|
||||
mutesAdapter.updateMutingNotificationsMap(mutingNotificationsMap)
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -99,11 +99,19 @@ interface MastodonApi {
|
|||
@Query("exclude_types[]") excludes: Set<Notification.Type>?
|
||||
): Call<List<Notification>>
|
||||
|
||||
@GET("api/v1/markers")
|
||||
fun markersWithAuth(
|
||||
@Header("Authorization") auth: String,
|
||||
@Header(DOMAIN_HEADER) domain: String,
|
||||
@Query("timeline[]") timelines: List<String>
|
||||
): Single<Map<String, Marker>>
|
||||
|
||||
@GET("api/v1/notifications")
|
||||
fun notificationsWithAuth(
|
||||
@Header("Authorization") auth: String,
|
||||
@Header(DOMAIN_HEADER) domain: String
|
||||
): Call<List<Notification>>
|
||||
@Header(DOMAIN_HEADER) domain: String,
|
||||
@Query("since_id") sinceId: String?
|
||||
): Single<List<Notification>>
|
||||
|
||||
@POST("api/v1/notifications/clear")
|
||||
fun clearNotifications(): Call<ResponseBody>
|
||||
|
@ -261,7 +269,7 @@ interface MastodonApi {
|
|||
@GET("api/v1/accounts/{id}")
|
||||
fun account(
|
||||
@Path("id") accountId: String
|
||||
): Call<Account>
|
||||
): Single<Account>
|
||||
|
||||
/**
|
||||
* 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<Relationship>
|
||||
): Single<Relationship>
|
||||
|
||||
@POST("api/v1/accounts/{id}/unfollow")
|
||||
fun unfollowAccount(
|
||||
@Path("id") accountId: String
|
||||
): Call<Relationship>
|
||||
): Single<Relationship>
|
||||
|
||||
@POST("api/v1/accounts/{id}/block")
|
||||
fun blockAccount(
|
||||
@Path("id") accountId: String
|
||||
): Call<Relationship>
|
||||
): Single<Relationship>
|
||||
|
||||
@POST("api/v1/accounts/{id}/unblock")
|
||||
fun unblockAccount(
|
||||
@Path("id") accountId: String
|
||||
): Call<Relationship>
|
||||
): Single<Relationship>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/accounts/{id}/mute")
|
||||
fun muteAccount(
|
||||
@Path("id") accountId: String,
|
||||
@Field("notifications") notifications: Boolean
|
||||
): Call<Relationship>
|
||||
@Field("notifications") notifications: Boolean? = null
|
||||
): Single<Relationship>
|
||||
|
||||
@POST("api/v1/accounts/{id}/unmute")
|
||||
fun unmuteAccount(
|
||||
@Path("id") accountId: String
|
||||
): Call<Relationship>
|
||||
): Single<Relationship>
|
||||
|
||||
@GET("api/v1/accounts/relationships")
|
||||
fun relationships(
|
||||
@Query("id[]") accountIds: List<String>
|
||||
): Call<List<Relationship>>
|
||||
): Single<List<Relationship>>
|
||||
|
||||
@GET("api/v1/accounts/{id}/identity_proofs")
|
||||
fun identityProofs(
|
||||
@Path("id") accountId: String
|
||||
): Call<List<IdentityProof>>
|
||||
): Single<List<IdentityProof>>
|
||||
|
||||
@GET("api/v1/blocks")
|
||||
fun blocks(
|
||||
|
@ -505,30 +513,27 @@ interface MastodonApi {
|
|||
@Field("choices[]") choices: List<Int>
|
||||
): Single<Poll>
|
||||
|
||||
@POST("api/v1/accounts/{id}/block")
|
||||
fun blockAccountObservable(
|
||||
@Path("id") accountId: String
|
||||
): Single<Relationship>
|
||||
@GET("api/v1/announcements")
|
||||
fun listAnnouncements(
|
||||
@Query("with_dismissed") withDismissed: Boolean = true
|
||||
): Single<List<Announcement>>
|
||||
|
||||
@POST("api/v1/accounts/{id}/unblock")
|
||||
fun unblockAccountObservable(
|
||||
@Path("id") accountId: String
|
||||
): Single<Relationship>
|
||||
@POST("api/v1/announcements/{id}/dismiss")
|
||||
fun dismissAnnouncement(
|
||||
@Path("id") announcementId: String
|
||||
): Single<ResponseBody>
|
||||
|
||||
@POST("api/v1/accounts/{id}/mute")
|
||||
fun muteAccountObservable(
|
||||
@Path("id") accountId: String
|
||||
): Single<Relationship>
|
||||
@PUT("api/v1/announcements/{id}/reactions/{name}")
|
||||
fun addAnnouncementReaction(
|
||||
@Path("id") announcementId: String,
|
||||
@Path("name") name: String
|
||||
): Single<ResponseBody>
|
||||
|
||||
@POST("api/v1/accounts/{id}/unmute")
|
||||
fun unmuteAccountObservable(
|
||||
@Path("id") accountId: String
|
||||
): Single<Relationship>
|
||||
|
||||
@GET("api/v1/accounts/relationships")
|
||||
fun relationshipsObservable(
|
||||
@Query("id[]") accountIds: List<String>
|
||||
): Single<List<Relationship>>
|
||||
@DELETE("api/v1/announcements/{id}/reactions/{name}")
|
||||
fun removeAnnouncementReaction(
|
||||
@Path("id") announcementId: String,
|
||||
@Path("name") name: String
|
||||
): Single<ResponseBody>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/reports")
|
||||
|
@ -563,4 +568,11 @@ interface MastodonApi {
|
|||
@Query("following") following: Boolean? = null
|
||||
): Single<SearchResult>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/accounts/{id}/note")
|
||||
fun updateAccountNote(
|
||||
@Path("id") accountId: String,
|
||||
@Field("comment") note: String
|
||||
): Single<Relationship>
|
||||
|
||||
}
|
||||
|
|
|
@ -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<Relationship> {
|
||||
override fun onResponse(call: Call<Relationship>, response: Response<Relationship>) {}
|
||||
|
||||
override fun onFailure(call: Call<Relationship>, 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<Relationship> {
|
||||
override fun onResponse(call: Call<Relationship>, response: Response<Relationship>) {}
|
||||
|
||||
override fun onFailure(call: Call<Relationship>, 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<DeletedStatus> {
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -54,7 +54,7 @@ fun CharSequence.emojify(emojis: List<Emoji>?, 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<View>) : 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)
|
||||
|
|
|
@ -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<Pair<File, int[]>> 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<Downloader.EmojiDownloadListener> 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<File, int[]> 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<Pair<File, int[]>> 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<File, int[]> 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<File, Float, File> {
|
||||
// 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 <a href="https://github.com/square/okhttp/issues/259">OkHttp issue 259</a>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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<Pair<File, List<Int>>>? = 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<Pair<File, List<Int>>> {
|
||||
// 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<Int> {
|
||||
if (version == null) return listOf(0)
|
||||
return version.split(".").map {
|
||||
it.toIntOrNull() ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadFontFile(context: Context,
|
||||
okHttpClient: OkHttpClient): Observable<Float> {
|
||||
return Observable.create { emitter: ObservableEmitter<Float> ->
|
||||
// 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<Int>, versionB: List<Int>): 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")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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<Resource<Account>>()
|
||||
val relationshipData = MutableLiveData<Resource<Relationship>>()
|
||||
|
||||
val noteSaved = MutableLiveData<Boolean>()
|
||||
|
||||
private val identityProofData = MutableLiveData<List<IdentityProof>>()
|
||||
|
||||
val accountFieldData = combineOptionalLiveData(accountData, identityProofData) { accountRes, identityProofs ->
|
||||
identityProofs.orEmpty().map { Either.Left<IdentityProof, Field>(it) }
|
||||
.plus(accountRes?.data?.fields.orEmpty().map { Either.Right<IdentityProof, Field>(it) })
|
||||
.plus(accountRes?.data?.fields.orEmpty().map { Either.Right(it) })
|
||||
}
|
||||
|
||||
|
||||
private val callList: MutableList<Call<*>> = 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<Boolean>().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<Account> {
|
||||
override fun onResponse(call: Call<Account>,
|
||||
response: Response<Account>) {
|
||||
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<Account>, 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<List<Relationship>> {
|
||||
override fun onResponse(call: Call<List<Relationship>>,
|
||||
response: Response<List<Relationship>>) {
|
||||
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<List<Relationship>>, 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<List<IdentityProof>> {
|
||||
override fun onResponse(call: Call<List<IdentityProof>>,
|
||||
response: Response<List<IdentityProof>>) {
|
||||
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<List<IdentityProof>>, 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<Relationship> {
|
||||
override fun onResponse(call: Call<Relationship>,
|
||||
response: Response<Relationship>) {
|
||||
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<Relationship>, 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() {
|
||||
|
|
9
app/src/main/res/drawable/ic_bullhorn_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_bullhorn_24dp.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12,8H4A2,2 0,0 0,2 10V14A2,2 0,0 0,4 16H5V20A1,1 0,0 0,6 21H8A1,1 0,0 0,9 20V16H12L17,20V4L12,8M21.5,12C21.5,13.71 20.54,15.26 19,16V8C20.53,8.75 21.5,10.3 21.5,12Z" />
|
||||
</vector>
|
|
@ -168,16 +168,43 @@
|
|||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="accountFollowsYouTextView,accountBadgeTextView" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/accountNoteTextInputLayout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.Dense"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/labelBarrier"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/account_note_hint" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/saveNoteInfo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/account_note_saved"
|
||||
android:textColor="@color/tusky_blue"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/accountNoteTextInputLayout" />
|
||||
|
||||
<androidx.emoji.widget.EmojiTextView
|
||||
android:id="@+id/accountNoteTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:hyphenationFrequency="full"
|
||||
android:lineSpacingMultiplier="1.1"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingTop="2dp"
|
||||
android:textColor="?android:textColorTertiary"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
app:layout_constraintTop_toBottomOf="@id/labelBarrier"
|
||||
app:layout_constraintTop_toBottomOf="@id/saveNoteInfo"
|
||||
tools:text="This is a test description. Descriptions can be quite looooong." />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
|
@ -244,6 +271,7 @@
|
|||
android:text="@string/title_statuses"
|
||||
android:textColor="@color/account_tab_font_color"
|
||||
android:textSize="?attr/status_text_medium" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
|
|
39
app/src/main/res/layout/activity_announcements.xml
Normal file
39
app/src/main/res/layout/activity_announcements.xml
Normal file
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/toolbar_basic" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeRefreshLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/announcementsList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<com.keylesspalace.tusky.view.BackgroundMessageView
|
||||
android:id="@+id/errorMessageView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:src="@android:color/transparent"
|
||||
android:visibility="gone"
|
||||
tools:src="@drawable/elephant_error"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -193,14 +193,12 @@
|
|||
android:textSize="?attr/status_text_medium" />
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
<com.keylesspalace.tusky.view.EmojiPicker
|
||||
android:id="@+id/emojiView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorSurface"
|
||||
android:clipToPadding="false"
|
||||
android:elevation="12dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="16dp"
|
||||
|
@ -327,6 +325,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:textColor="?android:textColorTertiary"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
android:textStyle="bold"
|
||||
tools:text="500" />
|
||||
|
||||
<com.keylesspalace.tusky.components.compose.view.TootButton
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
android:id="@+id/activity_view_thread"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="com.keylesspalace.tusky.PreferencesActivity">
|
||||
tools:context="com.keylesspalace.tusky.components.preference.PreferencesActivity">
|
||||
|
||||
<include layout="@layout/toolbar_basic" />
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/tabPreferenceContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
@ -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" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/actionButton"
|
||||
|
@ -30,7 +30,7 @@
|
|||
android:layout_margin="16dp"
|
||||
android:src="@drawable/ic_plus_24dp" />
|
||||
|
||||
<com.google.android.material.transformation.TransformationChildCard
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/sheet"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -38,8 +38,7 @@
|
|||
android:layout_margin="16dp"
|
||||
android:visibility="invisible"
|
||||
app:cardBackgroundColor="?attr/colorSurface"
|
||||
app:cardElevation="2dp"
|
||||
app:layout_behavior="@string/fab_transformation_sheet_behavior">
|
||||
app:cardElevation="2dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="240dp"
|
||||
|
@ -76,6 +75,7 @@
|
|||
android:textColor="?attr/colorOnPrimary"
|
||||
android:textSize="?attr/status_text_large" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.transformation.TransformationChildCard>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -1,30 +1,35 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:parentTag="androidx.cardview.widget.CardView"
|
||||
tools:layout_height="wrap_content"
|
||||
tools:layout_width="match_parent"
|
||||
tools:layout_height="wrap_content">
|
||||
tools:parentTag="com.google.android.material.card.MaterialCardView">
|
||||
|
||||
<LinearLayout
|
||||
android:padding="8dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:orientation="vertical">
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/licenseCardName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-medium"/>
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:textColor="?android:attr/textColorSecondary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/licenseCardLicense"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="?android:attr/textColorTertiary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/licenseCardLink"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/tusky_blue"/>
|
||||
android:textColor="@color/tusky_blue" />
|
||||
</LinearLayout>
|
||||
|
||||
</merge>
|
|
@ -1,33 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/emoji_font_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
<include
|
||||
android:id="@+id/item_blobmoji"
|
||||
layout="@layout/item_emoji_pref" />
|
||||
|
||||
<include
|
||||
android:id="@+id/item_blobmoji"
|
||||
layout="@layout/item_emoji_pref" />
|
||||
<include
|
||||
android:id="@+id/item_twemoji"
|
||||
layout="@layout/item_emoji_pref" />
|
||||
|
||||
<include
|
||||
android:id="@+id/item_twemoji"
|
||||
layout="@layout/item_emoji_pref" />
|
||||
<include
|
||||
android:id="@+id/item_notoemoji"
|
||||
layout="@layout/item_emoji_pref" />
|
||||
|
||||
<include
|
||||
android:id="@+id/item_notoemoji"
|
||||
layout="@layout/item_emoji_pref" />
|
||||
|
||||
<include
|
||||
android:id="@+id/item_nomoji"
|
||||
layout="@layout/item_emoji_pref" />
|
||||
</LinearLayout>
|
||||
<include
|
||||
android:id="@+id/item_nomoji"
|
||||
layout="@layout/item_emoji_pref" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emoji_download_label"
|
||||
|
@ -39,9 +31,6 @@
|
|||
android:paddingEnd="24dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:text="@string/download_fonts"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/emoji_font_list" />
|
||||
android:textColor="?android:attr/textColorSecondary" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</LinearLayout>
|
41
app/src/main/res/layout/item_announcement.xml
Normal file
41
app/src/main/res/layout/item_announcement.xml
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.emoji.widget.EmojiTextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:lineSpacingMultiplier="1.1"
|
||||
android:padding="8dp"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/chipGroup"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/text">
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/addReactionChip"
|
||||
style="@style/Widget.MaterialComponents.Chip.Action"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:checkable="false"
|
||||
app:chipEndPadding="4dp"
|
||||
app:chipIcon="@drawable/ic_plus_24dp"
|
||||
app:chipSurfaceColor="@color/tusky_blue"
|
||||
app:textEndPadding="0dp"
|
||||
app:textStartPadding="0dp" />
|
||||
|
||||
</com.google.android.material.chip.ChipGroup>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -349,7 +349,6 @@
|
|||
<string name="compose_shortcut_long_label">تحرير تبويق</string>
|
||||
<string name="compose_shortcut_short_label">كتابة</string>
|
||||
<string name="notification_clear_text">هل تريد حقا مسح كافة إشعاراتك؟</string>
|
||||
<string name="poll_info_time_relative">%s متبقي</string>
|
||||
<string name="poll_info_time_absolute">ينتهي في %s</string>
|
||||
<string name="poll_info_closed">انتهى</string>
|
||||
<string name="poll_vote">صَوِّت</string>
|
||||
|
@ -384,36 +383,36 @@
|
|||
</plurals>
|
||||
<string name="pref_title_bot_overlay">إظهار علامة البوتات</string>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="zero">%d أيام</item>
|
||||
<item quantity="one">%d يوم</item>
|
||||
<item quantity="two">%d يومين</item>
|
||||
<item quantity="few">%d أيام</item>
|
||||
<item quantity="many">%d أيام</item>
|
||||
<item quantity="other">%d أيام</item>
|
||||
<item quantity="zero">%d أيام متبقية</item>
|
||||
<item quantity="one">%d يوم متبقي</item>
|
||||
<item quantity="two">%d يومين متبقيين</item>
|
||||
<item quantity="few">%d أيام متبقية</item>
|
||||
<item quantity="many">%d أيام متبقية</item>
|
||||
<item quantity="other">%d أيام متبقية</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="zero">%d ساعات</item>
|
||||
<item quantity="one">%d ساعة</item>
|
||||
<item quantity="two">%d ساعتين</item>
|
||||
<item quantity="few">%d ساعات</item>
|
||||
<item quantity="many">%d ساعات</item>
|
||||
<item quantity="other">%d ساعات</item>
|
||||
<item quantity="zero">%d ساعات متبقية</item>
|
||||
<item quantity="one">%d ساعة متبقية</item>
|
||||
<item quantity="two">%d ساعتان متبقيتان</item>
|
||||
<item quantity="few">%d ساعات متبقية</item>
|
||||
<item quantity="many">%d ساعات متبقية</item>
|
||||
<item quantity="other">%d ساعات متبقية</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="zero">%d دقائق</item>
|
||||
<item quantity="one">%d دقيقة</item>
|
||||
<item quantity="two">%d دقيقتين</item>
|
||||
<item quantity="few">%d دقائق</item>
|
||||
<item quantity="many">%d دقائق</item>
|
||||
<item quantity="other">%d دقائق</item>
|
||||
<item quantity="zero">%d دقائق متبقية</item>
|
||||
<item quantity="one">%d دقيقة متبقية</item>
|
||||
<item quantity="two">%d دقيقتان متبقيتان</item>
|
||||
<item quantity="few">%d دقائق متبقية</item>
|
||||
<item quantity="many">%d دقائق متبقية</item>
|
||||
<item quantity="other">%d دقائق متبقية</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="zero">%d ثوان</item>
|
||||
<item quantity="one">%d ثانية</item>
|
||||
<item quantity="two">%d ثانيتين</item>
|
||||
<item quantity="few">%d ثوان</item>
|
||||
<item quantity="many">%d ثوان</item>
|
||||
<item quantity="other">%d ثوان</item>
|
||||
<item quantity="zero">%d ثوان متبقية</item>
|
||||
<item quantity="one">%d ثانية متبقية</item>
|
||||
<item quantity="two">%d ثانيتان متبقيتان</item>
|
||||
<item quantity="few">%d ثوان متبقية</item>
|
||||
<item quantity="many">%d ثوان متبقية</item>
|
||||
<item quantity="other">%d ثوان متبقية</item>
|
||||
</plurals>
|
||||
<string name="compose_preview_image_description">إجراءات على الصورة %s</string>
|
||||
<string name="caption_notoemoji">حزمة الإيموجي الحالية لـ غوغل</string>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<string name="action_close">ⵎⴷⴻⵍ</string>
|
||||
<string name="error_generic">ⵝⴻⵍⵍⴰ ⴷ ⵝⵓⵛⴹⴰ.</string>
|
||||
<string name="title_lists">ⵝⴰⴲⴸⴰⵔⵉⵏ</string>
|
||||
<string name="action_lists">ⵝⴰⴲⴸⴰⵔⵉⵏ</string>
|
||||
<string name="action_lists">ⵝⵉⴲⴸⴰⵔⵉⵏ</string>
|
||||
<string name="about_title_activity">ⵖⴻⴼ</string>
|
||||
<string name="action_reset_schedule">ⵡⴻⵏⵏⴻⵣ ⵝⵉⴽⴻⵍⵜ ⵏⵏⵉⴸⴻⵏ</string>
|
||||
<string name="action_search">ⵏⴰⴸⵉ</string>
|
||||
|
@ -22,12 +22,22 @@
|
|||
<string name="button_continue">ⴽⴻⵎⵎⴻⵍ</string>
|
||||
<string name="filter_dialog_update_button">ⵍⵇⴻⵎ</string>
|
||||
<string name="filter_dialog_remove_button">ⴽⴽⴻⵙ</string>
|
||||
<string name="action_send_public">ⵊⴻⵡⵡⴻⵇ</string>
|
||||
<string name="action_send_public">ⵊⴻⵡⵡⴻⵇ!</string>
|
||||
<string name="action_send">ⵊⴻⵡⵡⴻⵇ</string>
|
||||
<string name="action_login">ⵇⵇⴻⵏ ⵖⴻⵔ ⵎⴰⵚⵟⵓⴷⵓⵏ</string>
|
||||
<string name="link_whats_an_instance">ⴸ ⴰⵛⵓ ⵓ ⵜⵙⵓⵎⵎⴰⵏⵜ\?</string>
|
||||
<string name="link_whats_an_instance">ⴸ ⴰⵛⵓ ⵓⴸ ⵜⵜⵓⵎⵎⴰⵏⵜ\?</string>
|
||||
<string name="notification_favourite_name">ⵉⵙⵎⴻⵏⵢⵉⴼⴻⵏ</string>
|
||||
<string name="action_remove">ⴽⴽⴻⵙ</string>
|
||||
<string name="action_access_saved_toot">ⵉⵔⴻⵡⵡⴰⵢⴻⵏ</string>
|
||||
<string name="title_blocks">ⵉⵎⵙⴻⵇⴷⴰⵛⴻⵏ ⵜⵙⵡⴰⵃⵍⴻⵎ</string>
|
||||
<string name="pref_title_status_tabs">ⵉⵛⵛⴰⵔⴻⵏ</string>
|
||||
<string name="hint_domain">ⴰⵏⵜⴰ ⵝⵓⵎⵎⴰⵏⵜ\?</string>
|
||||
<string name="add_account_description">ⵔⵏⵓ ⵢⵉⵡⴻⵏ ⵏ ⵓⵎⵉⴹⴰⵏ ⴰⵎⴰⵢⵏⵓⵝ ⵏ ⵎⴰⵚⵟⵓⴷⵓⵏ</string>
|
||||
<string name="add_account_name">ⵔⵏⵓ ⴰⵎⵉⴹⴰⵏ</string>
|
||||
<string name="hint_compose">ⴸⴰⵛⵓ ⵉⴳⴻⵍⵍⴰⵏ ⴸ ⴰⵎⴰⵢⵏⵓⵝ\?</string>
|
||||
<string name="action_schedule_toot">ⵙⵖⵉⵡⴻⵙ ⵝⵉⵊⴻⵡⵡⵉⵇⵝⴰ</string>
|
||||
<string name="action_access_scheduled_toot">ⵝⵉⵊⴻⵡⵡⵉⵇⵉⵏ ⵢⴻⵜⵜⵖⴰⵙⵖⴰⵡⵙⴻⵏ</string>
|
||||
<string name="title_scheduled_toot">ⵝⵉⵊⴻⵡⵡⵉⵇⵉⵏ ⵢⴻⵜⵜⵖⴰⵙⵖⴰⵡⵙⴻⵏ</string>
|
||||
<string name="title_bookmarks">ⵝⵉⵛⵔⴰⴹ</string>
|
||||
<string name="action_view_bookmarks">ⵝⵉⵛⵔⴰⴹ</string>
|
||||
</resources>
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="conversation_1_recipients">%1$s</string>
|
||||
<string name="abbreviated_seconds_ago">%ds</string>
|
||||
<string name="abbreviated_seconds_ago">%dসে</string>
|
||||
<string name="description_poll">পছন্দগুলি সহ নর্বাচন: %1$s, %2$s, %3$s, %4$s; %5$s</string>
|
||||
<string name="description_visiblity_direct">সরাসরি</string>
|
||||
<string name="description_visiblity_private">অনুগামিবৃন্দ</string>
|
||||
|
|
|
@ -366,7 +366,6 @@
|
|||
<string name="notification_clear_text">আপনি কি আপনার সমস্ত বিজ্ঞপ্তি স্থায়ীভাবে মুছে ফেলতে চান\?</string>
|
||||
<string name="compose_preview_image_description">ছবি %s এর জন্য ক্রিয়া</string>
|
||||
<string name="poll_info_format"> <!-- ১৫ ভোট • ১ ঘন্টা বাকি --> %1$s • %2$s</string>
|
||||
<string name="poll_info_time_relative">%s বাকি</string>
|
||||
<string name="poll_info_time_absolute">%s এ শেষ হবে</string>
|
||||
<string name="poll_info_closed">বন্ধ</string>
|
||||
<string name="poll_vote">ভোট</string>
|
||||
|
@ -424,8 +423,8 @@
|
|||
<string name="dialog_mute_warning">নিঃশব্দ @%s\?</string>
|
||||
<string name="dialog_block_warning">অবরুদ্ধ @%s\?</string>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="one">%d দিন</item>
|
||||
<item quantity="other">%d দিন</item>
|
||||
<item quantity="one">%d দিন বাকি</item>
|
||||
<item quantity="other">%d দিন বাকি</item>
|
||||
</plurals>
|
||||
<string name="pref_title_gradient_for_media">লুকানো মিডিয়ার জন্য রঙিন গ্রেডিয়েন্ট ব্যবহার করি</string>
|
||||
<string name="action_unmute_conversation">আলাপ বন্ধ করো</string>
|
||||
|
@ -437,16 +436,16 @@
|
|||
<string name="notification_follow_request_name">অনুরোধ অনুসরণ করুন</string>
|
||||
<string name="pref_title_confirm_reblogs">বুস্ট করার আগে নিশ্চিত করো</string>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one">%d সেকেন্ড</item>
|
||||
<item quantity="other">%d সেকেন্ড</item>
|
||||
<item quantity="one">%d সেকেন্ড বাকি</item>
|
||||
<item quantity="other">%d সেকেন্ড বাকি</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one">%d মিনিট</item>
|
||||
<item quantity="other">%d মিনিট</item>
|
||||
<item quantity="one">%d মিনিট বাকি</item>
|
||||
<item quantity="other">%d মিনিট বাকি</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one">%d ঘন্টা</item>
|
||||
<item quantity="other">%d ঘন্টা</item>
|
||||
<item quantity="one">%d ঘন্টা বাকি</item>
|
||||
<item quantity="other">%d ঘন্টা বাকি</item>
|
||||
</plurals>
|
||||
<plurals name="poll_info_people">
|
||||
<item quantity="one">%s জন</item>
|
||||
|
@ -470,4 +469,5 @@
|
|||
<string name="no_scheduled_status">তোমার কোনো সময়সূচীত স্ট্যাটাস নেই।</string>
|
||||
<string name="no_saved_status">তোমার কোনো খসড়া নেই।</string>
|
||||
<string name="warning_scheduling_interval">মাস্টোডনের সর্বনিম্ন ৫ মিনিটের সময়সূচীর বিরতি আছে।</string>
|
||||
<string name="pref_title_hide_top_toolbar">শীর্ষস্থানীয় সরঞ্জামদণ্ডের শিরোনামটি লুকান</string>
|
||||
</resources>
|
|
@ -330,12 +330,12 @@
|
|||
<string name="pref_title_absolute_time">Utilitzar el temps absolut</string>
|
||||
<string name="label_remote_account">La informació de sota pot mostrar el perfil incomplert de l\'usuari. Clica per obrir el perfil complert al navegador.</string>
|
||||
<plurals name="favs">
|
||||
<item quantity="one"></item>
|
||||
<item quantity="other"></item>
|
||||
<item quantity="one"><b>%1$s</b> Favorit</item>
|
||||
<item quantity="other"><b>%1$s</b> Favorits</item>
|
||||
</plurals>
|
||||
<plurals name="reblogs">
|
||||
<item quantity="one"></item>
|
||||
<item quantity="other"></item>
|
||||
<item quantity="one"/>
|
||||
<item quantity="other"/>
|
||||
</plurals>
|
||||
<string name="title_reblogged_by">Impulsat per</string>
|
||||
<string name="title_favourited_by">Marcat favorit per</string>
|
||||
|
@ -364,27 +364,10 @@
|
|||
<item quantity="one">%s vots</item>
|
||||
<item quantity="other">%s vots</item>
|
||||
</plurals>
|
||||
<string name="poll_info_time_relative">%s restants</string>
|
||||
<string name="poll_info_time_absolute">Acaba a %s</string>
|
||||
<string name="poll_info_closed">tancat</string>
|
||||
<string name="poll_ended_voted">L\'enquesta on has votat està tancada</string>
|
||||
<string name="poll_ended_created">La enquesta que heu creat ha finalitzat</string>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="one"/>
|
||||
<item quantity="other"/>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one"/>
|
||||
<item quantity="other"/>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one"/>
|
||||
<item quantity="other"/>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one"/>
|
||||
<item quantity="other"/>
|
||||
</plurals>
|
||||
<string name="description_status_cw">Advertència: %s</string>
|
||||
<string name="title_statuses_pinned">Toot fixat</string>
|
||||
<string name="unpin_action">Toot no fixat</string>
|
||||
|
|
|
@ -377,7 +377,6 @@
|
|||
<item quantity="few">%s hlasy</item>
|
||||
<item quantity="other">%s hlasů</item>
|
||||
</plurals>
|
||||
<string name="poll_info_time_relative">zbývá %s</string>
|
||||
<string name="poll_info_time_absolute">končí v %s</string>
|
||||
<string name="poll_info_closed">uzavřena</string>
|
||||
<string name="poll_vote">Hlasovat</string>
|
||||
|
@ -388,24 +387,19 @@
|
|||
<string name="poll_ended_voted">Anketa, ve které jste hlasoval/a, skončila</string>
|
||||
<string name="poll_ended_created">Anketa, kterou jste vytvořil/a, skončila</string>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="one"/>
|
||||
<item quantity="few"/>
|
||||
<item quantity="other"/>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one"/>
|
||||
<item quantity="few"/>
|
||||
<item quantity="other"/>
|
||||
<item quantity="one">zbývá %d den</item>
|
||||
<item quantity="few">zbývá %d dny</item>
|
||||
<item quantity="other">zbývá %d dní</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one">%d minuta</item>
|
||||
<item quantity="few">%d minut</item>
|
||||
<item quantity="other"/>
|
||||
<item quantity="one">zbývá %d minuta</item>
|
||||
<item quantity="few">zbývá %d minuty</item>
|
||||
<item quantity="other">zbývá %d minut</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one"/>
|
||||
<item quantity="few"/>
|
||||
<item quantity="other"/>
|
||||
<item quantity="one">zbývá %d sekunda</item>
|
||||
<item quantity="few">zbývá %d sekundy</item>
|
||||
<item quantity="other">zbývá %d sekund</item>
|
||||
</plurals>
|
||||
<string name="pref_title_animate_gif_avatars">Animovat avatary GIF</string>
|
||||
<string name="description_poll">Anketa s volbami: %1$s, %2$s, %3$s, %4$s; %5$s</string>
|
||||
|
@ -482,9 +476,9 @@
|
|||
<string name="action_unmute_desc">Odkrýt %s</string>
|
||||
<string name="dialog_mute_warning">Ztišit @%s\?</string>
|
||||
<plurals name="poll_info_people">
|
||||
<item quantity="one">%s osoba</item>
|
||||
<item quantity="few">%s osoby</item>
|
||||
<item quantity="other">%s osob</item>
|
||||
<item quantity="one"/>
|
||||
<item quantity="few"/>
|
||||
<item quantity="other"/>
|
||||
</plurals>
|
||||
<string name="notification_follow_request_format">%s požádal/a aby vás mohl/a sledovat</string>
|
||||
<string name="pref_title_confirm_reblogs">Zobrazit dialogové okno s potvrzením při boostování</string>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="error_generic">Ein Fehler ist aufgetreten.</string>
|
||||
<string name="error_network">Ein Netzwerkfehler ist aufgetreten! Bitte überprüfen deine Internetverbindung und versuche es erneut!</string>
|
||||
<string name="error_network">Ein Netzwerkfehler ist aufgetreten! Bitte überprüfen Sie Ihre Internetverbindung und versuchen Sie es erneut!</string>
|
||||
<string name="error_empty">Dies darf nicht leer sein.</string>
|
||||
<string name="error_invalid_domain">Ungültige Domain angegeben</string>
|
||||
<string name="error_failed_app_registration">Authentifizieren mit dieser Instanz fehlgeschlagen.</string>
|
||||
|
@ -10,7 +10,7 @@
|
|||
<string name="error_authorization_denied">Autorisierung fehlgeschlagen.</string>
|
||||
<string name="error_retrieving_oauth_token">Es konnte kein Login-Token abgerufen werden.</string>
|
||||
<string name="error_compose_character_limit">Der Beitrag ist zu lang!</string>
|
||||
<string name="error_image_upload_size">Die Datei muss kleiner als 8MB sein.</string>
|
||||
<string name="error_image_upload_size">Die Datei muss kleiner als 8 MB sein.</string>
|
||||
<string name="error_video_upload_size">Videodateien müssen kleiner als 40 MB sein.</string>
|
||||
<string name="error_media_upload_type">Dieser Dateityp darf nicht hochgeladen werden.</string>
|
||||
<string name="error_media_upload_opening">Die Datei konnte nicht geöffnet werden.</string>
|
||||
|
@ -18,7 +18,7 @@
|
|||
<string name="error_media_download_permission">Eine Berechtigung wird zum Speichern des Mediums benötigt.</string>
|
||||
<string name="error_media_upload_image_or_video">Bilder und Videos können nicht beide gleichzeitig an einen Beitrag angehängt werden.</string>
|
||||
<string name="error_media_upload_sending">Die Mediendatei konnte nicht hochgeladen werden.</string>
|
||||
<string name="error_sender_account_gone">Fehler beim Senden des Status</string>
|
||||
<string name="error_sender_account_gone">Fehler beim Senden des Beitrags.</string>
|
||||
<string name="title_home">Start</string>
|
||||
<string name="title_notifications">Benachrichtigungen</string>
|
||||
<string name="title_public_local">Lokal</string>
|
||||
|
@ -36,7 +36,7 @@
|
|||
<string name="title_blocks">Blockierte Profile</string>
|
||||
<string name="title_follow_requests">Folgeanfragen</string>
|
||||
<string name="title_edit_profile">Dein Profil bearbeiten</string>
|
||||
<string name="title_saved_toot">Gespeicherte Beiträge</string>
|
||||
<string name="title_saved_toot">Entwürfe</string>
|
||||
<string name="title_licenses">Lizenzen</string>
|
||||
<string name="status_username_format">\@%s</string>
|
||||
<string name="status_boosted_format">%s teilte</string>
|
||||
|
@ -59,7 +59,7 @@
|
|||
<string name="action_reblog">Teilen</string>
|
||||
<string name="action_unreblog">Boost entfernen</string>
|
||||
<string name="action_favourite">Favorisieren</string>
|
||||
<string name="action_unfavourite">Favoriten entfernen</string>
|
||||
<string name="action_unfavourite">Favorisierung entfernen</string>
|
||||
<string name="action_more">Mehr</string>
|
||||
<string name="action_compose">Beitrag erstellen</string>
|
||||
<string name="action_login">Anmelden mit Mastodon</string>
|
||||
|
@ -92,7 +92,7 @@
|
|||
<string name="action_mute">Stummschalten</string>
|
||||
<string name="action_unmute">Lautschalten</string>
|
||||
<string name="action_mention">Erwähnen</string>
|
||||
<string name="action_hide_media">Verstecke Medien</string>
|
||||
<string name="action_hide_media">Medien verstecken</string>
|
||||
<string name="action_open_drawer">Drawer öffnen</string>
|
||||
<string name="action_save">Speichern</string>
|
||||
<string name="action_edit_profile">Profil bearbeiten</string>
|
||||
|
@ -101,7 +101,7 @@
|
|||
<string name="action_accept">Akzeptieren</string>
|
||||
<string name="action_reject">Ablehnen</string>
|
||||
<string name="action_search">Suche</string>
|
||||
<string name="action_access_saved_toot">Gespeicherte Beiträge</string>
|
||||
<string name="action_access_saved_toot">Entwürfe</string>
|
||||
<string name="action_toggle_visibility">Beitragssichtbarkeit</string>
|
||||
<string name="action_content_warning">Inhaltswarnung</string>
|
||||
<string name="action_emoji_keyboard">Emoji</string>
|
||||
|
@ -149,7 +149,7 @@
|
|||
\n\nWeitere Informationen gibt es auf <a href="https://joinmastodon.org">joinmastodon.org</a>.
|
||||
</string>
|
||||
<string name="dialog_title_finishing_media_upload">Stelle Medienupload fertig</string>
|
||||
<string name="dialog_message_uploading_media">Lade hoch…</string>
|
||||
<string name="dialog_message_uploading_media">Lade hoch …</string>
|
||||
<string name="dialog_download_image">Herunterladen</string>
|
||||
<string name="dialog_message_cancel_follow_request">Folgeanfrage zurückziehen?</string>
|
||||
<string name="dialog_unfollow_warning">Willst du diesem Profil wirklich nicht mehr folgen?</string>
|
||||
|
@ -159,7 +159,7 @@
|
|||
<string name="visibility_private">Nur Folgende: Nur für Folgende sichtbar</string>
|
||||
<string name="visibility_direct">Direkt: Nur für Erwähnte sichtbar</string>
|
||||
<string name="pref_title_edit_notification_settings">Benachrichtigungen</string>
|
||||
<string name="pref_title_notifications_enabled">Benachrichtigungseinstellungen</string>
|
||||
<string name="pref_title_notifications_enabled">Benachrichtigungen</string>
|
||||
<string name="pref_title_notification_alerts">Benachrichtigungen</string>
|
||||
<string name="pref_title_notification_alert_sound">Benachrichtige mit Sound</string>
|
||||
<string name="pref_title_notification_alert_vibrate">Benachrichtige mit Vibration</string>
|
||||
|
@ -197,7 +197,7 @@
|
|||
<string name="pref_publishing">Beiträge</string>
|
||||
<string name="pref_failed_to_sync">Fehler beim Synchronisieren</string>
|
||||
<string name="post_privacy_public">Öffentlich</string>
|
||||
<string name="post_privacy_unlisted">Ungelistet</string>
|
||||
<string name="post_privacy_unlisted">Nicht gelistet</string>
|
||||
<string name="post_privacy_followers_only">Nur Folgende</string>
|
||||
<string name="pref_status_text_size">Schriftgröße</string>
|
||||
<string name="status_text_size_smallest">Kleiner</string>
|
||||
|
@ -208,10 +208,10 @@
|
|||
<string name="notification_mention_name">Neue Erwähnungen</string>
|
||||
<string name="notification_mention_descriptions">Benachrichtigungen über neue Erwähnungen</string>
|
||||
<string name="notification_follow_name">Neue Folgende</string>
|
||||
<string name="notification_follow_description">Benachrichtigungen über neue Folgende</string>
|
||||
<string name="notification_follow_description">Benachrichtigunen über neue Folgende</string>
|
||||
<string name="notification_boost_name">Geteilte Beiträge</string>
|
||||
<string name="notification_boost_description">Benachrichtigungen wenn deine Beiträge geteilt werden</string>
|
||||
<string name="notification_favourite_name">Favoriten</string>
|
||||
<string name="notification_favourite_name">Favorisierte Beiträge</string>
|
||||
<string name="notification_favourite_description">Benachrichtigungen wenn deine Beiträge favorisiert werden</string>
|
||||
<string name="notification_mention_format">%s hat dich erwähnt</string>
|
||||
<string name="notification_summary_large">%1$s, %2$s, %3$s und %4$d andere</string>
|
||||
|
@ -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.
|
||||
-->
|
||||
<string name="about_project_site">Webseite des Projekts:
|
||||
<string name="about_project_site">Website des Projekts:
|
||||
\n https://tusky.app</string>
|
||||
<string name="about_bug_feature_request_site"> Fehlermeldungen & Verbesserungsvorschläge:\n
|
||||
https://github.com/tuskyapp/Tusky/issues
|
||||
|
@ -286,10 +286,10 @@
|
|||
<string name="later">Später</string>
|
||||
<string name="restart">Neustarten</string>
|
||||
<string name="caption_systememoji">Die Standard-Emojis deines Geräts</string>
|
||||
<string name="caption_blobmoji">Die Blob-Emojis aus Android 4.4-7.1</string>
|
||||
<string name="caption_blobmoji">Die Blob–Emojis aus Android 4.4–7.1</string>
|
||||
<string name="caption_twemoji">Die Standard-Emojis von Mastodon</string>
|
||||
<string name="caption_notoemoji">Die aktuellen Emojis von Google</string>
|
||||
<string name="download_failed">Download fehlgeschlagen</string>
|
||||
<string name="download_failed">Download fehlgeschlagen.</string>
|
||||
<string name="profile_badge_bot_text">Bot</string>
|
||||
<string name="account_moved_description">%1$s ist umgezogen auf:</string>
|
||||
<string name="reblog_private">An ursprüngliches Publikum teilen</string>
|
||||
|
@ -354,34 +354,33 @@
|
|||
<item quantity="one">%s Stimme</item>
|
||||
<item quantity="other">%s Stimmen</item>
|
||||
</plurals>
|
||||
<string name="poll_info_time_relative">%s verbleibend</string>
|
||||
<string name="poll_info_time_absolute">endet um %s</string>
|
||||
<string name="poll_info_closed">Geschlossen</string>
|
||||
<string name="poll_vote">Abstimmen</string>
|
||||
<string name="poll_ended_voted">Eine Umfrage in der du abgestimmt hast ist vorbei</string>
|
||||
<string name="poll_ended_created">Eine Umfrage, die du erstellt hast, ist vorbei</string>
|
||||
<string name="poll_ended_created">Eine Umfrage die du erstellt hast ist vorbei</string>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="one">%d Tag</item>
|
||||
<item quantity="other">%d Tage</item>
|
||||
<item quantity="one">%d Tag verbleibend</item>
|
||||
<item quantity="other">%d Tage verbleibend</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one">%d Stunde</item>
|
||||
<item quantity="other">%d Stunden</item>
|
||||
<item quantity="one">%d Stunde verbleibend</item>
|
||||
<item quantity="other">%d Stunden verbleibend</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one">%d Minute</item>
|
||||
<item quantity="other">%d Minuten</item>
|
||||
<item quantity="one">%d Minute verbleibend</item>
|
||||
<item quantity="other">%d Minuten verbleibend</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one">%d Sekunde</item>
|
||||
<item quantity="other">%d Sekunden</item>
|
||||
<item quantity="one">%d Sekunde verbleibend</item>
|
||||
<item quantity="other">%d Sekunden verbleibend</item>
|
||||
</plurals>
|
||||
<string name="title_domain_mutes">Versteckte Domains</string>
|
||||
<string name="action_view_domain_mutes">Versteckte Domains</string>
|
||||
<string name="action_mute_domain">%s verstecken</string>
|
||||
<string name="confirmation_domain_unmuted">%s nicht mehr versteckt</string>
|
||||
<string name="mute_domain_warning">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.</string>
|
||||
<string name="mute_domain_warning_dialog_ok">ganze Domain verbergen</string>
|
||||
<string name="mute_domain_warning_dialog_ok">Ganze Domain verbergen</string>
|
||||
<string name="pref_title_animate_gif_avatars">GIF-Avatare animieren</string>
|
||||
<string name="filter_dialog_whole_word">Ganzes Wort</string>
|
||||
<string name="filter_dialog_whole_word_description">Wenn das Schlagwort nur aus Buchstaben und Zahlen besteht, wird es nur angewendet, wenn es dem ganzen Wort entspricht</string>
|
||||
|
@ -404,7 +403,7 @@
|
|||
<string name="poll_duration_3_days">3 Tage</string>
|
||||
<string name="poll_duration_7_days">7 Tage</string>
|
||||
<string name="edit_poll">Editieren</string>
|
||||
<string name="about_tusky_version">Tusky %s</string>
|
||||
<string name="about_tusky_version">test %s</string>
|
||||
<string name="action_add_poll">Umfrage hinzufügen</string>
|
||||
<string name="pref_title_alway_open_spoiler">Beiträge mit Inhaltswarnungen immer ausklappen</string>
|
||||
<string name="description_poll">Umfrage mit den Möglichkeiten: %1$s, %2$s, %3$s, %4$s; %5$s</string>
|
||||
|
@ -420,7 +419,7 @@
|
|||
<string name="action_access_scheduled_toot">Geplante Beiträge</string>
|
||||
<string name="action_schedule_toot">Plane Beitrag</string>
|
||||
<string name="action_reset_schedule">Zurücksetzen</string>
|
||||
<string name="error_audio_upload_size">Audiodateien müssen kleiner als 40MB sein.</string>
|
||||
<string name="error_audio_upload_size">Audiodateien müssen kleiner als 40 MB sein.</string>
|
||||
<string name="title_bookmarks">Lesezeichen</string>
|
||||
<string name="action_bookmark">Lesezeichen</string>
|
||||
<string name="action_view_bookmarks">Lesezeichen</string>
|
||||
|
@ -452,7 +451,7 @@
|
|||
<string name="pref_title_gradient_for_media">Farbverlauf für versteckte Medien anzeigen</string>
|
||||
<string name="pref_main_nav_position_option_bottom">Unten</string>
|
||||
<string name="pref_main_nav_position_option_top">Oben</string>
|
||||
<string name="action_unmute_domain">Ton einschalten %s</string>
|
||||
<string name="action_unmute_domain">%s nicht mehr verstecken</string>
|
||||
<plurals name="reblogs">
|
||||
<item quantity="one"><b>%s</b> Boost</item>
|
||||
<item quantity="other"><b>%s</b> Boosts</item>
|
||||
|
@ -461,5 +460,14 @@
|
|||
<string name="dialog_mute_hide_notifications">Benachrichtigungen ausblenden</string>
|
||||
<string name="action_mute_notifications_desc">Benachrichtigungen stummschalten von %s</string>
|
||||
<string name="action_unmute_notifications_desc">Benachrichtigungs-Ton einschalten von %s</string>
|
||||
<string name="action_unmute_desc">Ton einschalten %s</string>
|
||||
<string name="action_unmute_desc">%s nicht mehr verstecken</string>
|
||||
<string name="abbreviated_seconds_ago">%d Sek.</string>
|
||||
<string name="abbreviated_hours_ago">%d St.</string>
|
||||
<string name="abbreviated_in_days">in %d T.</string>
|
||||
<string name="abbreviated_years_ago">%d J.</string>
|
||||
<string name="account_note_saved">Gespeichert!</string>
|
||||
<string name="account_note_hint">Private Notiz über diesen Account</string>
|
||||
<string name="pref_title_hide_top_toolbar">Titel der Hauptnavigation verstecken</string>
|
||||
<string name="no_announcements">Im Moment gibt es keine Ankündigungen.</string>
|
||||
<string name="title_announcements">Ankündigungen</string>
|
||||
</resources>
|
|
@ -376,28 +376,12 @@
|
|||
<item quantity="one"/>
|
||||
<item quantity="other"/>
|
||||
</plurals>
|
||||
<string name="poll_info_time_relative">%s restas</string>
|
||||
<string name="poll_info_time_absolute">finiĝos je %s</string>
|
||||
<string name="poll_info_closed">finiĝita</string>
|
||||
<string name="poll_vote">Voĉdoni</string>
|
||||
<string name="poll_ended_voted">Enketo al kiu vi voĉdonis finiĝis</string>
|
||||
<string name="poll_ended_created">Enketo kiu vi kreis finiĝis</string>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="one"/>
|
||||
<item quantity="other"/>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one"/>
|
||||
<item quantity="other"/>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one"/>
|
||||
<item quantity="other"/>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one"/>
|
||||
<item quantity="other"/>
|
||||
</plurals>
|
||||
|
||||
<string name="title_domain_mutes">Kaŝitaj domajnoj</string>
|
||||
<string name="action_view_domain_mutes">Kaŝitaj domajnoj</string>
|
||||
<string name="action_mute_domain">Silentigi %s</string>
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<string name="error_media_upload_opening">No pudo abrirse el fichero.</string>
|
||||
<string name="error_media_upload_permission">Se requiere permiso para acceder al almacenamiento.</string>
|
||||
<string name="error_media_download_permission">Se requiere permiso para descargar al almacenamiento.</string>
|
||||
<string name="error_media_upload_image_or_video">No se pueden adjuntar imágenes y vídeos en el mismo estado</string>
|
||||
<string name="error_media_upload_image_or_video">No se pueden adjuntar imágenes y vídeos en el mismo estado.</string>
|
||||
<string name="error_media_upload_sending">La subida falló.</string>
|
||||
<string name="error_sender_account_gone">Error al publicar.</string>
|
||||
<string name="title_home">Inicio</string>
|
||||
|
@ -72,7 +72,7 @@
|
|||
<string name="action_report">Reportar</string>
|
||||
<string name="action_delete">Borrar</string>
|
||||
<string name="action_send">Enviar</string>
|
||||
<string name="action_send_public">Publicar</string>
|
||||
<string name="action_send_public">¡Publicar!</string>
|
||||
<string name="action_retry">Reintentar</string>
|
||||
<string name="action_close">Cerrar</string>
|
||||
<string name="action_view_profile">Perfil</string>
|
||||
|
@ -116,7 +116,7 @@
|
|||
<string name="confirmation_unmuted">El usuario ya no está silenciado</string>
|
||||
<string name="status_sent">¡Enviado!</string>
|
||||
<string name="status_sent_long">Respuesta enviada correctamente.</string>
|
||||
<string name="hint_domain">Indique la instancia</string>
|
||||
<string name="hint_domain">¿Qué instancia\?</string>
|
||||
<string name="hint_compose">¿En qué estás pensando?</string>
|
||||
<string name="hint_content_warning">Aviso de contenido</string>
|
||||
<string name="hint_display_name">Nombre</string>
|
||||
|
@ -300,8 +300,8 @@
|
|||
<item quantity="other"><b>%1$s</b> Favoritos</item>
|
||||
</plurals>
|
||||
<plurals name="reblogs">
|
||||
<item quantity="one"><b>%s</b> Boost</item>
|
||||
<item quantity="other"><b>%s</b> Boosts</item>
|
||||
<item quantity="one"><b>%s</b> impulso</item>
|
||||
<item quantity="other"><b>%s</b> impulsos</item>
|
||||
</plurals>
|
||||
<string name="title_reblogged_by">Impulsado por</string>
|
||||
<string name="title_favourited_by">Marcado como favorito por</string>
|
||||
|
@ -330,20 +330,20 @@
|
|||
<string name="pref_title_bot_overlay">Mostrar indicador de bots</string>
|
||||
<string name="poll_vote">Votar</string>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="one"/>
|
||||
<item quantity="other"/>
|
||||
<item quantity="one">%d día restante</item>
|
||||
<item quantity="other">%d días restante</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one"/>
|
||||
<item quantity="other"/>
|
||||
<item quantity="one">%d hora restante</item>
|
||||
<item quantity="other">%d horas restante</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one">%d minuto</item>
|
||||
<item quantity="other">%d minutos</item>
|
||||
<item quantity="one">%d minuto restante</item>
|
||||
<item quantity="other">%d minutos restante</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one">%d segundo</item>
|
||||
<item quantity="other">%d segundos</item>
|
||||
<item quantity="one">%d segundo restante</item>
|
||||
<item quantity="other">%d segundos restante</item>
|
||||
</plurals>
|
||||
<string name="poll_info_format"> <!-- 15 votos • queda 1 hora --> %1$s • %2$s</string>
|
||||
<plurals name="poll_info_votes">
|
||||
|
@ -388,7 +388,6 @@
|
|||
<string name="compose_shortcut_short_label">Redactar</string>
|
||||
<string name="notification_clear_text">¿Estás seguro de que quieres eliminar permanentemente todas tus notificaciones\?</string>
|
||||
<string name="compose_preview_image_description">Acciones para la imagen %s</string>
|
||||
<string name="poll_info_time_relative">%s restante</string>
|
||||
<string name="poll_info_time_absolute">termina en %s</string>
|
||||
<string name="poll_ended_voted">Una encuesta en la que has votado ha terminado</string>
|
||||
<string name="poll_ended_created">Una encuesta que has creado ha terminado</string>
|
||||
|
@ -475,4 +474,5 @@
|
|||
<string name="dialog_mute_hide_notifications">Ocultar notificaciones</string>
|
||||
<string name="action_mute_notifications_desc">Silenciar notificaciones desde %s</string>
|
||||
<string name="action_unmute_notifications_desc">Dejar de silenciar notificaciones desde %s</string>
|
||||
<string name="pref_title_hide_top_toolbar">Ocultar el título de la barra de herramientas superior</string>
|
||||
</resources>
|
|
@ -45,7 +45,7 @@
|
|||
<string name="footer_empty">Edukirik ez. Arrastatu behera birkargatzeko!</string>
|
||||
<string name="notification_reblog_format">%s-(e)k zure tuta bultzatu du</string>
|
||||
<string name="notification_favourite_format">%s-(e)k zure tuta gogoko du</string>
|
||||
<string name="notification_follow_format">%s-(e)k jarraitu dizu</string>
|
||||
<string name="notification_follow_format">%s(e)k jarraitu zaitu</string>
|
||||
<string name="report_username_format">\@%s salatu</string>
|
||||
<string name="report_comment_hint">Informazio gehigarria?</string>
|
||||
<string name="action_quick_reply">Erantzun azkarra</string>
|
||||
|
@ -75,7 +75,7 @@
|
|||
<string name="action_view_favourites">Gogokoak</string>
|
||||
<string name="action_view_mutes">Isilduak</string>
|
||||
<string name="action_view_blocks">Blokeatuak</string>
|
||||
<string name="action_view_follow_requests">Eskariak</string>
|
||||
<string name="action_view_follow_requests">Eskakizunak</string>
|
||||
<string name="action_view_media">Multimedia</string>
|
||||
<string name="action_open_in_web">Nabigatzailean ireki</string>
|
||||
<string name="action_add_media">Multimedia erantsi</string>
|
||||
|
@ -129,7 +129,7 @@
|
|||
<string name="dialog_title_finishing_media_upload">Mediaren igoera bukatzen</string>
|
||||
<string name="dialog_message_uploading_media">Igotzen…</string>
|
||||
<string name="dialog_download_image">Jaitsi</string>
|
||||
<string name="dialog_message_cancel_follow_request">Jarraipen-eskaerari uko egin\?</string>
|
||||
<string name="dialog_message_cancel_follow_request">Jarraipen-eskakizunari uko egin\?</string>
|
||||
<string name="dialog_unfollow_warning">Kontu hau jarraitzeari utzi\?</string>
|
||||
<string name="dialog_delete_toot_warning">Tuta ezabatu\?</string>
|
||||
<string name="visibility_public">Publikoa: Istorio publikoetan erakutsi</string>
|
||||
|
@ -156,7 +156,7 @@
|
|||
<string name="app_theme_auto">Automatikoa</string>
|
||||
<string name="pref_title_browser_settings">Nabigatzailea</string>
|
||||
<string name="pref_title_custom_tabs">Chromeko fitxak erabili</string>
|
||||
<string name="pref_title_hide_follow_button">Tut egiteko botoia ezkutatu beherantz joaterakoan.</string>
|
||||
<string name="pref_title_hide_follow_button">Tut egiteko botoia ezkutatu beherantz joaterakoan</string>
|
||||
<string name="pref_title_status_filter">Denbora-lerro filtroak</string>
|
||||
<string name="pref_title_status_tabs">Fitxak</string>
|
||||
<string name="pref_title_show_boosts">Bultzadak erakutsi</string>
|
||||
|
@ -279,7 +279,7 @@
|
|||
<string name="pin_action">Ainguratu</string>
|
||||
<string name="error_network">Sareko errore bat sortu da! Zure konexioa ziurta ezazu berriro, mesedez!</string>
|
||||
<string name="title_direct_messages">Mezu Zuzenak</string>
|
||||
<string name="title_tab_preferences">Kategoriak</string>
|
||||
<string name="title_tab_preferences">Fitxak</string>
|
||||
<string name="title_statuses_pinned">Lotuta</string>
|
||||
<string name="title_domain_mutes">Ezkutuko domeinuak</string>
|
||||
<string name="title_scheduled_toot">Programatutako tutak</string>
|
||||
|
@ -291,7 +291,7 @@
|
|||
<string name="action_delete_and_redraft">Ezabatu eta zirriborroa berriro egin</string>
|
||||
<string name="action_view_domain_mutes">Ezkutuko domeinuak</string>
|
||||
<string name="action_add_poll">Galdeketa gehitu</string>
|
||||
<string name="action_mute_domain">%s isilarazi</string>
|
||||
<string name="action_mute_domain">Mututu %s</string>
|
||||
<string name="action_access_scheduled_toot">Programatutako tutak</string>
|
||||
<string name="action_schedule_toot">Tuta programatu</string>
|
||||
<string name="action_reset_schedule">Berrezarri</string>
|
||||
|
@ -361,7 +361,7 @@
|
|||
<string name="conversation_1_recipients">%1$s</string>
|
||||
<string name="conversation_2_recipients">%1$s eta %2$s</string>
|
||||
<string name="conversation_more_recipients">%1$s, %2$s eta %3$d gehiago</string>
|
||||
<string name="max_tab_number_reached">geienezko %1$d fitxa iritsita</string>
|
||||
<string name="max_tab_number_reached">gehienezko %1$d fitxa iritsita</string>
|
||||
<string name="description_status_media">Media: %s</string>
|
||||
<string name="description_status_cw">Edukiaren abisua: %s</string>
|
||||
<string name="description_status_media_no_description_placeholder">Deskribapenik ez</string>
|
||||
|
@ -383,8 +383,8 @@
|
|||
<string name="compose_preview_image_description">%s irudiarentzako ekintzak</string>
|
||||
<string name="poll_info_format"> <!-- 15 boto • Ordu 1 geratzen da --> %1$s • %2$s</string>
|
||||
<plurals name="poll_info_votes">
|
||||
<item quantity="one"/>
|
||||
<item quantity="other"/>
|
||||
<item quantity="one">Boto %s</item>
|
||||
<item quantity="other">%s Boto</item>
|
||||
</plurals>
|
||||
<string name="poll_info_time_absolute">%s amaitzen da</string>
|
||||
<string name="poll_info_closed">Itxita</string>
|
||||
|
@ -392,20 +392,16 @@
|
|||
<string name="poll_ended_voted">Botoa eman duzun galdeketa amaitu da</string>
|
||||
<string name="poll_ended_created">Sortu duzun galdeketa amaitu da</string>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="one">Egun %d</item>
|
||||
<item quantity="other">%d egun</item>
|
||||
<item quantity="one">Egun %d geratzen da</item>
|
||||
<item quantity="other">%d egun geratzen da</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one">Ordu %d</item>
|
||||
<item quantity="other">%d ordu</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one">Minutu %d</item>
|
||||
<item quantity="other">%d minutu</item>
|
||||
<item quantity="one">Ordu %d geratzen da</item>
|
||||
<item quantity="other">%d ordu geratzen da</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one">Segundu %d</item>
|
||||
<item quantity="other">%d segundu</item>
|
||||
<item quantity="one">Segundu %d geratzen da</item>
|
||||
<item quantity="other">%d segundu geratzen da</item>
|
||||
</plurals>
|
||||
<string name="button_continue">Jarraitu</string>
|
||||
<string name="button_back">Itzuli</string>
|
||||
|
@ -439,7 +435,6 @@
|
|||
<string name="action_open_reblogger">Ireki bultzadaren egilea</string>
|
||||
<string name="pref_title_public_filter_keywords">Denbora lerro publikoak</string>
|
||||
<string name="description_status_bookmarked">Laster-markatuta</string>
|
||||
<string name="poll_info_time_relative">%s geratzen da</string>
|
||||
<string name="error_audio_upload_size">Audioak 40MB baino gutxiago izan behar ditu.</string>
|
||||
<string name="select_list_title">Aukeratu zerrenda</string>
|
||||
<string name="list">Zerrenda</string>
|
||||
|
@ -450,13 +445,31 @@
|
|||
<string name="notification_follow_request_description">Jarraitzeko eskaereri buruzko jakinarazpenak</string>
|
||||
<string name="dialog_mute_warning">\@%s isildu\?</string>
|
||||
<string name="dialog_block_warning">\@%s blokeatu\?</string>
|
||||
<string name="action_mute_conversation">Elkarrizketa isildu</string>
|
||||
<string name="notification_follow_request_format">%s -k zu jarraitzeko eskatu dizu</string>
|
||||
<string name="action_mute_conversation">Mututu elkarrizketa</string>
|
||||
<string name="notification_follow_request_format">%s(e)k zu jarraitzeko eskatu dizu</string>
|
||||
<string name="hashtags">Traolak</string>
|
||||
<string name="dialog_mute_hide_notifications">Ez erakutsi jakinarazpenak</string>
|
||||
<string name="action_unmute_desc">Desmututu %s</string>
|
||||
<plurals name="poll_info_people">
|
||||
<item quantity="one">Pertsona %s</item>
|
||||
<item quantity="other">%s pertsona</item>
|
||||
<item quantity="one">Pertsona %1</item>
|
||||
<item quantity="other">%2 pertsona</item>
|
||||
</plurals>
|
||||
<string name="pref_title_hide_top_toolbar">Ezkutatu goiko tresna-barraren izenburua</string>
|
||||
<string name="pref_title_confirm_reblogs">Erakutsi berrespen-abisua tuta bultzatu aurretik</string>
|
||||
<string name="pref_title_show_cards_in_timelines">Erakutsi esteken aurrebista denbora-lerroetan</string>
|
||||
<string name="pref_title_enable_swipe_for_tabs">Gaitu pasatze-keinua fitxetan zehar aldatzeko</string>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one">Minutu %d faltan</item>
|
||||
<item quantity="other">%d minutu faltan</item>
|
||||
</plurals>
|
||||
<string name="add_hashtag_title">Gehitu traola</string>
|
||||
<string name="pref_main_nav_position_option_bottom">Azpia</string>
|
||||
<string name="pref_main_nav_position_option_top">Goia</string>
|
||||
<string name="pref_main_nav_position">Nabigatze posizio nagusia</string>
|
||||
<string name="pref_title_gradient_for_media">Erakutsi gradiente koloretsua ezkutuko mediarentzako</string>
|
||||
<string name="pref_title_notification_filter_follow_requests">jarraipena-eskaera</string>
|
||||
<string name="action_unmute_conversation">Desmututu elkarrizketa</string>
|
||||
<string name="action_unmute_domain">Desmututu %s</string>
|
||||
<string name="action_mute_notifications_desc">Mututu %s(r)en jakinarazpenak</string>
|
||||
<string name="action_unmute_notifications_desc">Desmututu %s(r)en jakinarazpenak</string>
|
||||
</resources>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue