diff --git a/app/build.gradle b/app/build.gradle
index 09564b26..19497d5d 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -20,8 +20,8 @@ android {
applicationId APP_ID
minSdkVersion 21
targetSdkVersion 29
- versionCode 75
- versionName "12.1"
+ versionCode 76
+ versionName "13.0 beta 1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
@@ -98,31 +98,31 @@ project.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
ext.lifecycleVersion = "2.2.0"
ext.roomVersion = '2.2.5'
ext.retrofitVersion = '2.9.0'
-ext.okhttpVersion = '4.8.1'
+ext.okhttpVersion = '4.9.0'
ext.glideVersion = '4.11.0'
-ext.daggerVersion = '2.28.3'
-ext.materialdrawerVersion = '8.1.4'
+ext.daggerVersion = '2.29.1'
+ext.materialdrawerVersion = '8.1.8'
// if libraries are changed here, they should also be changed in LicenseActivity
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
- implementation "androidx.core:core-ktx:1.3.1"
+ implementation "androidx.core:core-ktx:1.3.2"
implementation "androidx.appcompat:appcompat:1.2.0"
implementation "androidx.fragment:fragment-ktx:1.2.5"
implementation "androidx.browser:browser:1.2.0"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.recyclerview:recyclerview:1.1.0"
- implementation "androidx.exifinterface:exifinterface:1.2.0"
+ implementation "androidx.exifinterface:exifinterface:1.3.1"
implementation "androidx.cardview:cardview:1.0.0"
- implementation "androidx.preference:preference:1.1.1"
+ implementation "androidx.preference:preference-ktx:1.1.1"
implementation "androidx.sharetarget:sharetarget:1.0.0"
implementation "androidx.emoji:emoji:1.1.0"
implementation "androidx.emoji:emoji-appcompat:1.1.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycleVersion"
- implementation "androidx.constraintlayout:constraintlayout:1.1.3"
+ implementation "androidx.constraintlayout:constraintlayout:2.0.4"
implementation "androidx.paging:paging-runtime-ktx:2.1.2"
implementation "androidx.viewpager2:viewpager2:1.0.0"
implementation "androidx.work:work-runtime:2.4.0"
@@ -130,7 +130,7 @@ dependencies {
implementation "androidx.room:room-rxjava2:$roomVersion"
kapt "androidx.room:room-compiler:$roomVersion"
- implementation "com.google.android.material:material:1.2.0"
+ implementation "com.google.android.material:material:1.2.1"
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
@@ -144,7 +144,7 @@ dependencies {
implementation "com.github.bumptech.glide:glide:$glideVersion"
implementation "com.github.bumptech.glide:okhttp3-integration:$glideVersion"
- implementation "io.reactivex.rxjava2:rxjava:2.2.19"
+ implementation "io.reactivex.rxjava2:rxjava:2.2.20"
implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
implementation "io.reactivex.rxjava2:rxkotlin:2.4.0"
@@ -169,14 +169,12 @@ dependencies {
implementation "de.c1710:filemojicompat:1.0.17"
- testImplementation "androidx.test.ext:junit:1.1.1"
- testImplementation "org.robolectric:robolectric:4.3.1"
- testImplementation "org.mockito:mockito-inline:3.3.3"
+ testImplementation "androidx.test.ext:junit:1.1.2"
+ testImplementation "org.robolectric:robolectric:4.4"
+ testImplementation "org.mockito:mockito-inline:3.6.0"
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
- androidTestImplementation "androidx.test.espresso:espresso-core:3.2.0"
- androidTestImplementation "android.arch.persistence.room:testing:1.1.1"
- androidTestImplementation "androidx.test.ext:junit:1.1.1"
-
- debugImplementation "im.dino:dbinspector:4.0.0@aar"
+ androidTestImplementation "androidx.test.espresso:espresso-core:3.3.0"
+ androidTestImplementation "androidx.room:room-testing:$roomVersion"
+ androidTestImplementation "androidx.test.ext:junit:1.1.2"
}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index e25a4564..78b4be88 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -55,6 +55,9 @@
public static *** v(...);
public static *** i(...);
}
+-assumenosideeffects class java.lang.String {
+ public static java.lang.String format(...);
+}
# remove some kotlin overhead
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
@@ -62,8 +65,3 @@
static void checkExpressionValueIsNotNull(java.lang.Object, java.lang.String);
static void throwUninitializedPropertyAccessException(java.lang.String);
}
-
-# without this emoji font downloading fails with AbstractMethodError
--keep class * extends android.os.AsyncTask {
- public *;
-}
diff --git a/app/src/androidTest/java/com/keylesspalace/tusky/TimelineDAOTest.kt b/app/src/androidTest/java/com/keylesspalace/tusky/TimelineDAOTest.kt
index 241781b6..da55b08b 100644
--- a/app/src/androidTest/java/com/keylesspalace/tusky/TimelineDAOTest.kt
+++ b/app/src/androidTest/java/com/keylesspalace/tusky/TimelineDAOTest.kt
@@ -78,46 +78,39 @@ class TimelineDAOTest {
fun cleanup() {
val now = System.currentTimeMillis()
val oldDate = now - TimelineRepository.CLEANUP_INTERVAL - 20_000
- val oldByThisAccount = makeStatus(
+ val oldThisAccount = makeStatus(
statusId = 5,
createdAt = oldDate
)
- val oldByAnotherAccount = makeStatus(
+ val oldAnotherAccount = makeStatus(
statusId = 10,
createdAt = oldDate,
- authorServerId = "100"
+ accountId = 2
)
- val oldForAnotherAccount = makeStatus(
- accountId = 2,
- statusId = 20,
- authorServerId = "200",
- createdAt = oldDate
- )
- val recentByThisAccount = makeStatus(
+ val recentThisAccount = makeStatus(
statusId = 30,
createdAt = System.currentTimeMillis()
)
- val recentByAnotherAccount = makeStatus(
+ val recentAnotherAccount = makeStatus(
statusId = 60,
createdAt = System.currentTimeMillis(),
- authorServerId = "200"
- )
+ accountId = 2
+ )
- for ((status, author, reblogAuthor) in listOf(oldByThisAccount, oldByAnotherAccount,
- oldForAnotherAccount, recentByThisAccount, recentByAnotherAccount)) {
+ for ((status, author, reblogAuthor) in listOf(oldThisAccount, oldAnotherAccount, recentThisAccount, recentAnotherAccount)) {
timelineDao.insertInTransaction(status, author, reblogAuthor)
}
- timelineDao.cleanup(1, "20", now - TimelineRepository.CLEANUP_INTERVAL)
+ timelineDao.cleanup(now - TimelineRepository.CLEANUP_INTERVAL)
assertEquals(
- listOf(recentByAnotherAccount, recentByThisAccount, oldByThisAccount),
+ listOf(recentThisAccount),
timelineDao.getStatusesForAccount(1, null, null, 100).blockingGet()
.map { it.toTriple() }
)
assertEquals(
- listOf(oldForAnotherAccount),
+ listOf(recentAnotherAccount),
timelineDao.getStatusesForAccount(2, null, null, 100).blockingGet()
.map { it.toTriple() }
)
@@ -217,7 +210,8 @@ class TimelineDAOTest {
application = "application$accountId",
reblogServerId = if (reblog) (statusId * 100).toString() else null,
reblogAccountId = reblogAuthor?.serverId,
- poll = null
+ poll = null,
+ muted = false
)
return Triple(status, author, reblogAuthor)
}
@@ -246,7 +240,8 @@ class TimelineDAOTest {
application = null,
reblogServerId = null,
reblogAccountId = null,
- poll = null
+ poll = null,
+ muted = false
)
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 53564ace..585ff833 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -36,7 +36,7 @@
+ android:configChanges="orientation|screenSize|keyboardHidden" />
@@ -105,7 +105,7 @@
+ android:windowSoftInputMode="stateVisible|adjustResize" />
@@ -117,7 +117,7 @@
android:name=".AccountActivity"
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize" />
-
+
@@ -145,6 +145,7 @@
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
+
+ tools:node="remove" />
-
\ No newline at end of file
+
diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt
index 09781875..478d8aff 100644
--- a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt
@@ -23,6 +23,7 @@ import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.os.Bundle
+import android.text.Editable
import android.view.Menu
import android.view.MenuItem
import android.view.View
@@ -34,7 +35,6 @@ import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat
import androidx.emoji.text.EmojiCompat
-import androidx.lifecycle.Observer
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.viewpager2.widget.MarginPageTransformer
@@ -132,6 +132,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
if (viewModel.isSelf) {
updateButtons()
+ saveNoteInfo.hide()
+ } else {
+ saveNoteInfo.visibility = View.INVISIBLE
}
}
@@ -311,7 +314,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
* Subscribe to data loaded at the view model
*/
private fun subscribeObservables() {
- viewModel.accountData.observe(this, Observer {
+ viewModel.accountData.observe(this) {
when (it) {
is Success -> onAccountChanged(it.data)
is Error -> {
@@ -320,8 +323,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
.show()
}
}
- })
- viewModel.relationshipData.observe(this, Observer {
+ }
+ viewModel.relationshipData.observe(this) {
val relation = it?.data
if (relation != null) {
onRelationshipChanged(relation)
@@ -333,12 +336,14 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
.show()
}
- })
- viewModel.accountFieldData.observe(this, Observer {
+ }
+ viewModel.accountFieldData.observe(this, {
accountFieldAdapter.fields = it
accountFieldAdapter.notifyDataSetChanged()
-
})
+ viewModel.noteSaved.observe(this) {
+ saveNoteInfo.visible(it, View.INVISIBLE)
+ }
}
/**
@@ -349,7 +354,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
viewModel.refresh()
adapter.refreshContent()
}
- viewModel.isRefreshing.observe(this, Observer { isRefreshing ->
+ viewModel.isRefreshing.observe(this, { isRefreshing ->
swipeToRefreshLayout.isRefreshing = isRefreshing == true
})
swipeToRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
@@ -407,7 +412,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
accountAvatarImageView.setOnClickListener { avatarView ->
- val intent = ViewMediaActivity.newAvatarIntent(avatarView.context, account.avatar)
+ val intent = ViewMediaActivity.newSingleImageIntent(avatarView.context, account.avatar)
avatarView.transitionName = account.avatar
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this, avatarView, account.avatar)
@@ -533,9 +538,22 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
accountFollowsYouTextView.visible(relation.followedBy)
+ accountNoteTextInputLayout.visible(relation.note != null)
+ accountNoteTextInputLayout.editText?.setText(relation.note)
+
+ // add the listener late to avoid it firing on the first change
+ accountNoteTextInputLayout.editText?.removeTextChangedListener(noteWatcher)
+ accountNoteTextInputLayout.editText?.addTextChangedListener(noteWatcher)
+
updateButtons()
}
+ private val noteWatcher = object: DefaultTextWatcher() {
+ override fun afterTextChanged(s: Editable) {
+ viewModel.noteChanged(s.toString())
+ }
+ }
+
private fun updateFollowButton() {
if (viewModel.isSelf) {
accountFollowButton.setText(R.string.action_edit_own_profile)
@@ -705,9 +723,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
loadedAccount?.let {
showMuteAccountDialog(
this,
- it.username,
- { notifications -> viewModel.muteAccount(notifications) }
- )
+ it.username
+ ) { notifications ->
+ viewModel.muteAccount(notifications)
+ }
}
} else {
viewModel.unmuteAccount()
diff --git a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt
index 22d25f09..3d86b6e1 100644
--- a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt
@@ -30,8 +30,8 @@ import android.widget.ImageView
import androidx.activity.viewModels
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
+import androidx.core.view.isVisible
import androidx.lifecycle.LiveData
-import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.FitCenter
@@ -40,8 +40,6 @@ import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.adapter.AccountFieldEditAdapter
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
-import com.keylesspalace.tusky.entity.Account
-import com.keylesspalace.tusky.entity.Instance
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.viewmodel.EditProfileViewModel
import com.mikepenz.iconics.IconicsDrawable
@@ -112,7 +110,7 @@ class EditProfileActivity : BaseActivity(), Injectable {
addFieldButton.setOnClickListener {
accountFieldEditAdapter.addField()
if(accountFieldEditAdapter.itemCount >= MAX_ACCOUNT_FIELDS) {
- it.isEnabled = false
+ it.isVisible = false
}
scrollView.post{
@@ -122,7 +120,7 @@ class EditProfileActivity : BaseActivity(), Injectable {
viewModel.obtainProfile()
- viewModel.profileData.observe(this, Observer> { profileRes ->
+ viewModel.profileData.observe(this) { profileRes ->
when (profileRes) {
is Success -> {
val me = profileRes.data
@@ -163,10 +161,10 @@ class EditProfileActivity : BaseActivity(), Injectable {
}
}
- })
+ }
viewModel.obtainInstance()
- viewModel.instanceData.observe(this, Observer> { result ->
+ viewModel.instanceData.observe(this) { result ->
when (result) {
is Success -> {
val instance = result.data
@@ -175,12 +173,12 @@ class EditProfileActivity : BaseActivity(), Injectable {
}
}
}
- })
+ }
observeImage(viewModel.avatarData, avatarPreview, avatarProgressBar, true)
observeImage(viewModel.headerData, headerPreview, headerProgressBar, false)
- viewModel.saveData.observe(this, Observer> {
+ viewModel.saveData.observe(this, {
when(it) {
is Success -> {
finish()
@@ -215,7 +213,7 @@ class EditProfileActivity : BaseActivity(), Injectable {
imageView: ImageView,
progressBar: View,
roundedCorners: Boolean) {
- liveData.observe(this, Observer> {
+ liveData.observe(this, {
when (it) {
is Success -> {
diff --git a/app/src/main/java/com/keylesspalace/tusky/EmojiPreference.java b/app/src/main/java/com/keylesspalace/tusky/EmojiPreference.java
deleted file mode 100644
index 8837c7d8..00000000
--- a/app/src/main/java/com/keylesspalace/tusky/EmojiPreference.java
+++ /dev/null
@@ -1,291 +0,0 @@
-package com.keylesspalace.tusky;
-
-import android.app.AlarmManager;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Build;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.ImageButton;
-import android.widget.ImageView;
-import android.widget.ProgressBar;
-import android.widget.RadioButton;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import androidx.appcompat.app.AlertDialog;
-import androidx.preference.Preference;
-import androidx.preference.PreferenceManager;
-
-import com.keylesspalace.tusky.util.EmojiCompatFont;
-
-import java.util.ArrayList;
-
-/**
- * This Preference lets the user select their preferred emoji font
- */
-public class EmojiPreference extends Preference {
- private static final String TAG = "EmojiPreference";
- private EmojiCompatFont selected, original;
- static final String FONT_PREFERENCE = "selected_emoji_font";
- private static final EmojiCompatFont[] FONTS = EmojiCompatFont.FONTS;
- // Please note that this array should be sorted in the same way as their fonts.
- private static final int[] viewIds = {
- R.id.item_nomoji,
- R.id.item_blobmoji,
- R.id.item_twemoji,
- R.id.item_notoemoji};
-
- private ArrayList radioButtons = new ArrayList<>();
-
- private boolean updated, currentNeedsUpdate;
-
- public EmojiPreference(Context context) {
- super(context);
-
- // Find out which font is currently active
- this.selected = EmojiCompatFont.byId(PreferenceManager
- .getDefaultSharedPreferences(context)
- .getInt(FONT_PREFERENCE, 0));
- // We'll use this later to determine if anything has changed
- this.original = this.selected;
-
- setSummary(selected.getDisplay(context));
- }
-
- public EmojiPreference(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- // Find out which font is currently active
- this.selected = EmojiCompatFont.byId(PreferenceManager
- .getDefaultSharedPreferences(context)
- .getInt(FONT_PREFERENCE, 0));
- // We'll use this later to determine if anything has changed
- this.original = this.selected;
-
- setSummary(selected.getDisplay(context));
- }
-
- @Override
- protected void onClick() {
-
- View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_emojicompat, null);
-
- for (int i = 0; i < viewIds.length; i++) {
- setupItem(view.findViewById(viewIds[i]), FONTS[i]);
- }
-
- new AlertDialog.Builder(getContext())
- .setView(view)
- .setPositiveButton(android.R.string.ok, (dialog, which) -> onDialogOk())
- .setNegativeButton(android.R.string.cancel, null)
- .show();
- }
-
- private void setupItem(View container, EmojiCompatFont font) {
- Context context = container.getContext();
-
- TextView title = container.findViewById(R.id.emojicompat_name);
- TextView caption = container.findViewById(R.id.emojicompat_caption);
- ImageView thumb = container.findViewById(R.id.emojicompat_thumb);
- ImageButton download = container.findViewById(R.id.emojicompat_download);
- ImageButton cancel = container.findViewById(R.id.emojicompat_download_cancel);
- RadioButton radio = container.findViewById(R.id.emojicompat_radio);
-
- // Initialize all the views
- title.setText(font.getDisplay(context));
- caption.setText(font.getCaption(context));
- thumb.setImageDrawable(font.getThumb(context));
-
- // There needs to be a list of all the radio buttons in order to uncheck them when one is selected
- radioButtons.add(radio);
-
- updateItem(font, container);
-
- // Set actions
- download.setOnClickListener((downloadButton) ->
- startDownload(font, container));
-
- cancel.setOnClickListener((cancelButton) ->
- cancelDownload(font, container));
-
- radio.setOnClickListener((radioButton) ->
- select(font, (RadioButton) radioButton));
-
- container.setOnClickListener((containterView) ->
- select(font,
- containterView.findViewById(R.id.emojicompat_radio
- )));
- }
-
- private void startDownload(EmojiCompatFont font, View container) {
- ImageButton download = container.findViewById(R.id.emojicompat_download);
- TextView caption = container.findViewById(R.id.emojicompat_caption);
-
- ProgressBar progressBar = container.findViewById(R.id.emojicompat_progress);
- ImageButton cancel = container.findViewById(R.id.emojicompat_download_cancel);
-
- // Switch to downloading style
- download.setVisibility(View.GONE);
- caption.setVisibility(View.INVISIBLE);
- progressBar.setVisibility(View.VISIBLE);
- cancel.setVisibility(View.VISIBLE);
-
-
- font.downloadFont(getContext(), new EmojiCompatFont.Downloader.EmojiDownloadListener() {
- @Override
- public void onDownloaded(EmojiCompatFont font) {
- finishDownload(font, container);
- }
-
- @Override
- public void onProgress(float progress) {
- // The progress is returned as a float between 0 and 1
- progress *= progressBar.getMax();
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- progressBar.setProgress((int) progress, true);
- } else {
- progressBar.setProgress((int) progress);
- }
- }
-
- @Override
- public void onFailed() {
- Toast.makeText(getContext(), R.string.download_failed, Toast.LENGTH_SHORT).show();
- updateItem(font, container);
- }
- });
- }
-
- private void cancelDownload(EmojiCompatFont font, View container) {
- font.cancelDownload();
- updateItem(font, container);
- }
-
- private void finishDownload(EmojiCompatFont font, View container) {
- select(font, container.findViewById(R.id.emojicompat_radio));
- updateItem(font, container);
- // Set the flag to restart the app (because an update has been downloaded)
- if (selected == original && currentNeedsUpdate) {
- updated = true;
- currentNeedsUpdate = false;
- }
- }
-
- /**
- * Select a font both visually and logically
- *
- * @param font The font to be selected
- * @param radio The radio button associated with it's visual item
- */
- private void select(EmojiCompatFont font, RadioButton radio) {
- selected = font;
- // Uncheck all the other buttons
- for (RadioButton other : radioButtons) {
- if (other != radio) {
- other.setChecked(false);
- }
- }
- radio.setChecked(true);
- }
-
- /**
- * Called when a "consistent" state is reached, i.e. it's not downloading the font
- *
- * @param font The font to be displayed
- * @param container The ConstraintLayout containing the item
- */
- private void updateItem(EmojiCompatFont font, View container) {
- // Assignments
- ImageButton download = container.findViewById(R.id.emojicompat_download);
- TextView caption = container.findViewById(R.id.emojicompat_caption);
-
- ProgressBar progress = container.findViewById(R.id.emojicompat_progress);
- ImageButton cancel = container.findViewById(R.id.emojicompat_download_cancel);
-
- RadioButton radio = container.findViewById(R.id.emojicompat_radio);
-
- // There's no download going on
- progress.setVisibility(View.GONE);
- cancel.setVisibility(View.GONE);
- caption.setVisibility(View.VISIBLE);
-
- if (font.isDownloaded(getContext())) {
- // Make it selectable
- download.setVisibility(View.GONE);
- radio.setVisibility(View.VISIBLE);
- container.setClickable(true);
- } else {
- // Make it downloadable
- download.setVisibility(View.VISIBLE);
- radio.setVisibility(View.GONE);
- container.setClickable(false);
- }
-
- // Select it if necessary
- if (font == selected) {
- radio.setChecked(true);
- // Update available
- if (!font.isDownloaded(getContext())) {
- currentNeedsUpdate = true;
- }
- } else {
- radio.setChecked(false);
- }
- }
-
-
- /**
- * In order to be able to use this font later on, it needs to be saved first.
- */
- private void saveSelectedFont() {
- int index = selected.getId();
- Log.i(TAG, "saveSelectedFont: Font ID: " + index);
- // It's saved using the key FONT_PREFERENCE
- PreferenceManager
- .getDefaultSharedPreferences(getContext())
- .edit()
- .putInt(FONT_PREFERENCE, index)
- .apply();
- setSummary(selected.getDisplay(getContext()));
- }
-
- /**
- * That's it. The user doesn't want to switch between these amazing radio buttons anymore!
- * That means, the selected font can be saved (if the user hit OK)
- */
- private void onDialogOk() {
- saveSelectedFont();
- if (selected != original || updated) {
- new AlertDialog.Builder(getContext())
- .setTitle(R.string.restart_required)
- .setMessage(R.string.restart_emoji)
- .setNegativeButton(R.string.later, null)
- .setPositiveButton(R.string.restart, ((dialog, which) -> {
- // Restart the app
- // From https://stackoverflow.com/a/17166729/5070653
- Intent launchIntent = new Intent(getContext(), SplashActivity.class);
- PendingIntent mPendingIntent = PendingIntent.getActivity(
- getContext(),
- // This is the codepoint of the party face emoji :D
- 0x1f973,
- launchIntent,
- PendingIntent.FLAG_CANCEL_CURRENT);
- AlarmManager mgr =
- (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE);
- if (mgr != null) {
- mgr.set(
- AlarmManager.RTC,
- System.currentTimeMillis() + 100,
- mPendingIntent);
- }
- System.exit(0);
- })).show();
- }
-
- }
-
-}
diff --git a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt
index 163d9166..3894f652 100644
--- a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt
@@ -41,10 +41,8 @@ import com.keylesspalace.tusky.viewmodel.ListsViewModel.Event.*
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.*
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
-import com.mikepenz.iconics.utils.color
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
-import com.mikepenz.iconics.utils.toIconicsColor
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
import com.uber.autodispose.autoDispose
import dagger.android.DispatchingAndroidInjector
diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt
index 32222251..dc66945d 100644
--- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt
@@ -19,7 +19,6 @@ import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.res.ColorStateList
-import android.content.res.Configuration
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.net.Uri
@@ -29,7 +28,6 @@ import android.view.KeyEvent
import android.view.MenuItem
import android.view.View
import android.widget.ImageView
-import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AlertDialog
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
@@ -40,16 +38,22 @@ import androidx.lifecycle.Lifecycle
import androidx.preference.PreferenceManager
import androidx.viewpager2.widget.MarginPageTransformer
import com.bumptech.glide.Glide
+import com.bumptech.glide.RequestManager
+import com.bumptech.glide.load.resource.bitmap.RoundedCorners
+import com.bumptech.glide.request.target.CustomTarget
+import com.bumptech.glide.request.target.FixedSizeDrawable
+import com.bumptech.glide.request.transition.Transition
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
import com.google.android.material.tabs.TabLayoutMediator
-import com.google.android.material.tabs.TabLayoutMediator.TabConfigurationStrategy
import com.keylesspalace.tusky.appstore.*
+import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType
import com.keylesspalace.tusky.components.conversation.ConversationsRepository
import com.keylesspalace.tusky.components.notifications.NotificationHelper
+import com.keylesspalace.tusky.components.preference.PreferencesActivity
import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity
import com.keylesspalace.tusky.components.search.SearchActivity
import com.keylesspalace.tusky.db.AccountEntity
@@ -65,6 +69,9 @@ import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
+import com.mikepenz.materialdrawer.holder.BadgeStyle
+import com.mikepenz.materialdrawer.holder.ColorHolder
+import com.mikepenz.materialdrawer.holder.StringHolder
import com.mikepenz.materialdrawer.iconics.iconicsIcon
import com.mikepenz.materialdrawer.model.*
import com.mikepenz.materialdrawer.model.interfaces.*
@@ -74,6 +81,7 @@ import com.uber.autodispose.android.lifecycle.autoDispose
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.schedulers.Schedulers
import kotlinx.android.synthetic.main.activity_main.*
import javax.inject.Inject
@@ -91,13 +99,16 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
lateinit var conversationRepository: ConversationsRepository
private lateinit var header: AccountHeaderView
- private lateinit var drawerToggle: ActionBarDrawerToggle
private var notificationTabPosition = 0
private var onTabSelectedListener: OnTabSelectedListener? = null
+ private var unreadAnnouncementsCount = 0
+
private val preferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) }
+ private lateinit var glide: RequestManager
+
private val emojiInitCallback = object : InitCallback() {
override fun onInitialized() {
if (!isDestroyed) {
@@ -108,7 +119,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- if (accountManager.activeAccount == null) {
+
+ val activeAccount = accountManager.activeAccount
+ if (activeAccount == null) {
// will be redirected to LoginActivity by BaseActivity
return
}
@@ -161,6 +174,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
window.statusBarColor = Color.TRANSPARENT // don't draw a status bar, the DrawerLayout and the MaterialDrawerLayout have their own
setContentView(R.layout.activity_main)
+
+ glide = Glide.with(this)
+
composeButton.setOnClickListener {
val composeIntent = Intent(applicationContext, ComposeActivity::class.java)
startActivity(composeIntent)
@@ -169,6 +185,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
val hideTopToolbar = preferences.getBoolean(PrefKeys.HIDE_TOP_TOOLBAR, false)
mainToolbar.visible(!hideTopToolbar)
+ loadDrawerAvatar(activeAccount.profilePictureUrl, true)
+
mainToolbar.menu.add(R.string.action_search).apply {
setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
icon = IconicsDrawable(this@MainActivity, GoogleMaterial.Icon.gmd_search).apply {
@@ -187,6 +205,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
* drawer, though, because its callback touches the header in the drawer. */
fetchUserInfo()
+ fetchAnnouncements()
+
setupTabs(showNotificationTab)
// Setup push notifications
@@ -202,11 +222,17 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
when (event) {
is ProfileEditedEvent -> onFetchUserInfoSuccess(event.newProfileData)
is MainTabsChangedEvent -> setupTabs(false)
+ is AnnouncementReadEvent -> {
+ unreadAnnouncementsCount--
+ updateAnnouncementsBadge()
+ }
}
}
- // Flush old media that was cached for sharing
- deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Tusky"))
+ Schedulers.io().scheduleDirect {
+ // Flush old media that was cached for sharing
+ deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Tusky"))
+ }
}
override fun onResume() {
@@ -261,8 +287,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
public override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
- drawerToggle.syncState()
-
if (intent != null) {
val statusUrl = intent.getStringExtra(STATUS_URL)
if (statusUrl != null) {
@@ -288,7 +312,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
private fun setupDrawer(savedInstanceState: Bundle?, addSearchButton: Boolean) {
- drawerToggle = ActionBarDrawerToggle(this, mainDrawerLayout, mainToolbar, com.mikepenz.materialdrawer.R.string.material_drawer_open, com.mikepenz.materialdrawer.R.string.material_drawer_close)
+ mainToolbar.setNavigationOnClickListener {
+ mainDrawerLayout.open()
+ }
header = AccountHeaderView(this).apply {
headerBackgroundScaleType = ImageView.ScaleType.CENTER_CROP
@@ -312,13 +338,11 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) {
if (animateAvatars) {
- Glide.with(imageView.context)
- .load(uri)
+ glide.load(uri)
.placeholder(placeholder)
.into(imageView)
} else {
- Glide.with(imageView.context)
- .asBitmap()
+ glide.asBitmap()
.load(uri)
.placeholder(placeholder)
.into(imageView)
@@ -326,7 +350,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
override fun cancel(imageView: ImageView) {
- Glide.with(imageView.context).clear(imageView)
+ glide.clear(imageView)
}
override fun placeholder(ctx: Context, tag: String?): Drawable {
@@ -388,6 +412,18 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
startActivityWithSlideInAnimation(ScheduledTootActivity.newIntent(context))
}
},
+ primaryDrawerItem {
+ identifier = DRAWER_ITEM_ANNOUNCEMENTS
+ nameRes = R.string.title_announcements
+ iconRes = R.drawable.ic_bullhorn_24dp
+ onClick = {
+ startActivityWithSlideInAnimation(AnnouncementsActivity.newIntent(context))
+ }
+ badgeStyle = BadgeStyle().apply {
+ textColor = ColorHolder.fromColor(ThemeUtils.getColor(this@MainActivity, R.attr.colorOnPrimary))
+ color = ColorHolder.fromColor(ThemeUtils.getColor(this@MainActivity, R.attr.colorPrimary))
+ }
+ },
DividerDrawerItem(),
secondaryDrawerItem {
nameRes = R.string.action_view_account_preferences
@@ -420,7 +456,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
)
- if(addSearchButton) {
+ if (addSearchButton) {
mainDrawer.addItemsAtPosition(4,
primaryDrawerItem {
nameRes = R.string.action_search
@@ -446,25 +482,13 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
EmojiCompat.get().registerInitCallback(emojiInitCallback)
}
- override fun onConfigurationChanged(newConfig: Configuration) {
- super.onConfigurationChanged(newConfig)
- drawerToggle.onConfigurationChanged(newConfig)
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- if (drawerToggle.onOptionsItemSelected(item)) {
- return true
- }
- return super.onOptionsItemSelected(item)
- }
-
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(mainDrawer.saveInstanceState(outState))
}
private fun setupTabs(selectNotificationTab: Boolean) {
- val activeTabLayout = if(preferences.getString("mainNavPosition", "top") == "bottom") {
+ val activeTabLayout = if (preferences.getString("mainNavPosition", "top") == "bottom") {
val actionBarSize = ThemeUtils.getDimension(this, R.attr.actionBarSize)
val fabMargin = resources.getDimensionPixelSize(R.dimen.fabMargin)
(composeButton.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = actionBarSize + fabMargin
@@ -481,7 +505,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
val adapter = MainPagerAdapter(tabs, this)
viewPager.adapter = adapter
- TabLayoutMediator(activeTabLayout, viewPager, TabConfigurationStrategy { _: TabLayout.Tab?, _: Int -> }).attach()
+ TabLayoutMediator(activeTabLayout, viewPager) { _: TabLayout.Tab?, _: Int -> }.attach()
activeTabLayout.removeAllTabs()
for (i in tabs.indices) {
val tab = activeTabLayout.newTab()
@@ -614,11 +638,12 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
private fun onFetchUserInfoSuccess(me: Account) {
- Glide.with(this)
- .asBitmap()
+ glide.asBitmap()
.load(me.header)
.into(header.accountHeaderBackground)
+ loadDrawerAvatar(me.avatar, false)
+
accountManager.updateActiveAccount(me)
NotificationHelper.createNotificationChannelsForAccount(accountManager.activeAccount!!, this)
@@ -642,6 +667,55 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
updateShortcut(this, accountManager.activeAccount!!)
}
+ private fun loadDrawerAvatar(avatarUrl: String, showPlaceholder: Boolean) {
+ val navIconSize = resources.getDimensionPixelSize(R.dimen.avatar_toolbar_nav_icon_size)
+
+ glide.asDrawable()
+ .load(avatarUrl)
+ .transform(
+ RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
+ )
+ .apply {
+ if (showPlaceholder) {
+ placeholder(R.drawable.avatar_default)
+ }
+ }
+ .into(object : CustomTarget(navIconSize, navIconSize) {
+
+ override fun onLoadStarted(placeholder: Drawable?) {
+ if(placeholder != null) {
+ mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize)
+ }
+ }
+ override fun onResourceReady(resource: Drawable, transition: Transition?) {
+ mainToolbar.navigationIcon = resource
+ }
+
+ override fun onLoadCleared(placeholder: Drawable?) {
+ mainToolbar.navigationIcon = placeholder
+ }
+ })
+ }
+
+ private fun fetchAnnouncements() {
+ mastodonApi.listAnnouncements(false)
+ .observeOn(AndroidSchedulers.mainThread())
+ .autoDispose(this, Lifecycle.Event.ON_DESTROY)
+ .subscribe(
+ { announcements ->
+ unreadAnnouncementsCount = announcements.count { !it.read }
+ updateAnnouncementsBadge()
+ },
+ {
+ Log.w(TAG, "Failed to fetch announcements.", it)
+ }
+ )
+ }
+
+ private fun updateAnnouncementsBadge() {
+ mainDrawer.updateBadge(DRAWER_ITEM_ANNOUNCEMENTS, StringHolder(if (unreadAnnouncementsCount == 0) null else unreadAnnouncementsCount.toString()))
+ }
+
private fun updateProfiles() {
val profiles: MutableList = accountManager.getAllAccountsOrderedByActive().map { acc ->
val emojifiedName = EmojiCompat.get().process(acc.displayName.emojify(acc.emojis, header))
@@ -676,6 +750,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
private const val TAG = "MainActivity" // logging tag
private const val DRAWER_ITEM_ADD_ACCOUNT: Long = -13
private const val DRAWER_ITEM_FOLLOW_REQUESTS: Long = 10
+ private const val DRAWER_ITEM_ANNOUNCEMENTS: Long = 14
const val STATUS_URL = "statusUrl"
}
}
@@ -705,4 +780,4 @@ private var AbstractDrawerItem<*, *>.onClick: () -> Unit
value()
false
}
- }
\ No newline at end of file
+ }
diff --git a/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java b/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java
index 21cc9399..9a163989 100644
--- a/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java
+++ b/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java
@@ -164,6 +164,7 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd
List descriptions = gson.fromJson(item.getDescriptions(), stringListType);
ComposeOptions composeOptions = new ComposeOptions(
+ /*scheduledTootUid*/null,
item.getUid(),
item.getText(),
jsonUrls,
@@ -178,7 +179,8 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd
/*mediaAttachments*/null,
/*scheduledAt*/null,
/*sensitive*/null,
- /*poll*/null
+ /*poll*/null,
+ /* modifiedInitialState */ true
);
Intent intent = ComposeActivity.startIntent(this, composeOptions);
startActivity(intent);
diff --git a/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt
index ad622ea7..6b1aaef9 100644
--- a/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt
@@ -15,19 +15,25 @@
package com.keylesspalace.tusky
+import android.graphics.Color
import android.os.Bundle
import android.util.Log
import android.view.MenuItem
+import android.view.View
import android.widget.FrameLayout
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.AppCompatEditText
+import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.lifecycle.Lifecycle
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
+import androidx.transition.TransitionManager
import at.connyduck.sparkbutton.helpers.Utils
+import com.google.android.material.transition.MaterialArcMotion
+import com.google.android.material.transition.MaterialContainerTransform
import com.keylesspalace.tusky.adapter.ItemInteractionListener
import com.keylesspalace.tusky.adapter.ListSelectionAdapter
import com.keylesspalace.tusky.adapter.TabAdapter
@@ -78,7 +84,7 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
setDisplayShowHomeEnabled(true)
}
- currentTabs = (accountManager.activeAccount?.tabPreferences ?: emptyList()).toMutableList()
+ currentTabs = accountManager.activeAccount?.tabPreferences.orEmpty().toMutableList()
currentTabsAdapter = TabAdapter(currentTabs, false, this, currentTabs.size <= MIN_TAB_COUNT)
currentTabsRecyclerView.adapter = currentTabsAdapter
currentTabsRecyclerView.layoutManager = LinearLayoutManager(this)
@@ -129,19 +135,17 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
touchHelper.attachToRecyclerView(currentTabsRecyclerView)
-
actionButton.setOnClickListener {
- actionButton.isExpanded = true
+ toggleFab(true)
}
scrim.setOnClickListener {
- actionButton.isExpanded = false
+ toggleFab(false)
}
maxTabsInfo.text = getString(R.string.max_tab_number_reached, MAX_TAB_COUNT)
updateAvailableTabs()
-
}
override fun onTabAdded(tab: TabData) {
@@ -150,7 +154,7 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
return
}
- actionButton.isExpanded = false
+ toggleFab(false)
if (tab.id == HASHTAG) {
showAddHashtagDialog()
@@ -175,20 +179,36 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
saveTabs()
}
- override fun onActionChipClicked(tab: TabData) {
- showAddHashtagDialog(tab)
+ override fun onActionChipClicked(tab: TabData, tabPosition: Int) {
+ showAddHashtagDialog(tab, tabPosition)
}
- override fun onChipClicked(tab: TabData, chipPosition: Int) {
+ override fun onChipClicked(tab: TabData, tabPosition: Int, chipPosition: Int) {
val newArguments = tab.arguments.filterIndexed { i, _ -> i != chipPosition }
val newTab = tab.copy(arguments = newArguments)
- val position = currentTabs.indexOf(tab)
- currentTabs[position] = newTab
+ currentTabs[tabPosition] = newTab
+ saveTabs()
- currentTabsAdapter.notifyItemChanged(position)
+ currentTabsAdapter.notifyItemChanged(tabPosition)
}
- private fun showAddHashtagDialog(tab: TabData? = null) {
+ private fun toggleFab(expand: Boolean) {
+ val transition = MaterialContainerTransform().apply {
+ startView = if (expand) actionButton else sheet
+ val endView: View = if (expand) sheet else actionButton
+ this.endView = endView
+ addTarget(endView)
+ scrimColor = Color.TRANSPARENT
+ setPathMotion(MaterialArcMotion())
+ }
+
+ TransitionManager.beginDelayedTransition(tabPreferenceContainer, transition)
+ actionButton.visible(!expand)
+ sheet.visible(expand)
+ scrim.visible(expand)
+ }
+
+ private fun showAddHashtagDialog(tab: TabData? = null, tabPosition: Int = 0) {
val frameLayout = FrameLayout(this)
val padding = Utils.dpToPx(this, 8)
@@ -211,10 +231,9 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
currentTabsAdapter.notifyItemInserted(currentTabs.size - 1)
} else {
val newTab = tab.copy(arguments = tab.arguments + input)
- val position = currentTabs.indexOf(tab)
- currentTabs[position] = newTab
+ currentTabs[tabPosition] = newTab
- currentTabsAdapter.notifyItemChanged(position)
+ currentTabsAdapter.notifyItemChanged(tabPosition)
}
updateAvailableTabs()
@@ -319,10 +338,10 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
}
override fun onBackPressed() {
- if (actionButton.isExpanded) {
- actionButton.isExpanded = false
- } else {
+ if (actionButton.isVisible) {
super.onBackPressed()
+ } else {
+ toggleFab(false)
}
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt
index f41395bd..850c4b0d 100644
--- a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt
@@ -24,11 +24,16 @@ import androidx.preference.PreferenceManager
import androidx.work.WorkManager
import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory
import com.keylesspalace.tusky.di.AppInjector
-import com.keylesspalace.tusky.util.*
+import com.keylesspalace.tusky.settings.PrefKeys
+import com.keylesspalace.tusky.util.EmojiCompatFont
+import com.keylesspalace.tusky.util.LocaleManager
+import com.keylesspalace.tusky.util.ThemeUtils
import com.uber.autodispose.AutoDisposePlugins
+import dagger.Lazy
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import io.reactivex.plugins.RxJavaPlugins
+import io.reactivex.schedulers.Schedulers
import org.conscrypt.Conscrypt
import java.security.Security
import javax.inject.Inject
@@ -37,11 +42,21 @@ class TuskyApplication : Application(), HasAndroidInjector {
@Inject
lateinit var androidInjector: DispatchingAndroidInjector
+
@Inject
- lateinit var notificationWorkerFactory: NotificationWorkerFactory
+ lateinit var notificationWorkerFactory: Lazy
override fun onCreate() {
-
+ // Uncomment me to get StrictMode violation logs
+// if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+// StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
+// .detectDiskReads()
+// .detectDiskWrites()
+// .detectNetwork()
+// .detectUnbufferedIo()
+// .penaltyLog()
+// .build())
+// }
super.onCreate()
Security.insertProviderAt(Conscrypt.newProvider(), 1)
@@ -53,7 +68,7 @@ class TuskyApplication : Application(), HasAndroidInjector {
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
// init the custom emoji fonts
- val emojiSelection = preferences.getInt(EmojiPreference.FONT_PREFERENCE, 0)
+ val emojiSelection = preferences.getInt(PrefKeys.EMOJI, 0)
val emojiConfig = EmojiCompatFont.byId(emojiSelection)
.getConfig(this)
.setReplaceAll(true)
@@ -63,16 +78,19 @@ class TuskyApplication : Application(), HasAndroidInjector {
val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT)
ThemeUtils.setAppNightMode(theme)
- WorkManager.initialize(
- this,
- androidx.work.Configuration.Builder()
- .setWorkerFactory(notificationWorkerFactory)
- .build()
- )
-
RxJavaPlugins.setErrorHandler {
Log.w("RxJava", "undeliverable exception", it)
}
+
+ // This will initialize the whole network stack and cache so we don't wan to wait for it
+ Schedulers.computation().scheduleDirect {
+ WorkManager.initialize(
+ this,
+ androidx.work.Configuration.Builder()
+ .setWorkerFactory(notificationWorkerFactory.get())
+ .build()
+ )
+ }
}
override fun attachBaseContext(base: Context) {
diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt
index 447c0590..5d90bd94 100644
--- a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt
@@ -46,7 +46,7 @@ import com.bumptech.glide.request.FutureTarget
import com.keylesspalace.tusky.BuildConfig.APPLICATION_ID
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.fragment.ViewImageFragment
-import com.keylesspalace.tusky.pager.AvatarImagePagerAdapter
+import com.keylesspalace.tusky.pager.SingleImagePagerAdapter
import com.keylesspalace.tusky.pager.ImagePagerAdapter
import com.keylesspalace.tusky.util.getTemporaryMediaFilename
import com.keylesspalace.tusky.viewdata.AttachmentViewData
@@ -68,7 +68,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
companion object {
private const val EXTRA_ATTACHMENTS = "attachments"
private const val EXTRA_ATTACHMENT_INDEX = "index"
- private const val EXTRA_AVATAR_URL = "avatar"
+ private const val EXTRA_SINGLE_IMAGE_URL = "single_image"
private const val TAG = "ViewMediaActivity"
@JvmStatic
@@ -79,9 +79,10 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
return intent
}
- fun newAvatarIntent(context: Context, url: String): Intent {
+ @JvmStatic
+ fun newSingleImageIntent(context: Context, url: String): Intent {
val intent = Intent(context, ViewMediaActivity::class.java)
- intent.putExtra(EXTRA_AVATAR_URL, url)
+ intent.putExtra(EXTRA_SINGLE_IMAGE_URL, url)
return intent
}
}
@@ -91,6 +92,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
private var attachments: ArrayList? = null
private val toolbarVisibilityListeners = mutableListOf()
+ private var imageUrl: String? = null
fun addToolbarVisibilityListener(listener: ToolbarVisibilityListener): Function0 {
this.toolbarVisibilityListeners.add(listener)
@@ -117,10 +119,10 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
ImagePagerAdapter(this, realAttachs, initialPosition)
} else {
- val avatarUrl = intent.getStringExtra(EXTRA_AVATAR_URL)
- ?: throw IllegalArgumentException("attachment list or avatar url has to be set")
+ imageUrl = intent.getStringExtra(EXTRA_SINGLE_IMAGE_URL)
+ ?: throw IllegalArgumentException("attachment list or image url has to be set")
- AvatarImagePagerAdapter(this, avatarUrl)
+ SingleImagePagerAdapter(this, imageUrl!!)
}
viewPager.adapter = adapter
@@ -161,11 +163,10 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
- if (attachments != null) {
- menuInflater.inflate(R.menu.view_media_toolbar, menu)
- return true
- }
- return false
+ menuInflater.inflate(R.menu.view_media_toolbar, menu)
+ // We don't support 'open status' from single image views
+ menu?.findItem(R.id.action_open_status)?.isVisible = (attachments != null)
+ return true
}
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
@@ -213,7 +214,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
}
private fun downloadMedia() {
- val url = attachments!![viewPager.currentItem].attachment.url
+ val url = imageUrl ?: attachments!![viewPager.currentItem].attachment.url
val filename = Uri.parse(url).lastPathSegment
Toast.makeText(applicationContext, resources.getString(R.string.download_image, filename), Toast.LENGTH_SHORT).show()
@@ -240,8 +241,9 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
}
private fun copyLink() {
+ val url = imageUrl ?: attachments!![viewPager.currentItem].attachment.url
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
- clipboard.setPrimaryClip(ClipData.newPlainText(null, attachments!![viewPager.currentItem].attachment.url))
+ clipboard.setPrimaryClip(ClipData.newPlainText(null, url))
}
private fun shareMedia() {
@@ -251,13 +253,17 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
return
}
- val attachment = attachments!![viewPager.currentItem].attachment
- when (attachment.type) {
- Attachment.Type.IMAGE -> shareImage(directory, attachment.url)
- Attachment.Type.AUDIO,
- Attachment.Type.VIDEO,
- Attachment.Type.GIFV -> shareMediaFile(directory, attachment.url)
- else -> Log.e(TAG, "Unknown media format for sharing.")
+ if (imageUrl != null) {
+ shareImage(directory, imageUrl!!)
+ } else {
+ val attachment = attachments!![viewPager.currentItem].attachment
+ when (attachment.type) {
+ Attachment.Type.IMAGE -> shareImage(directory, attachment.url)
+ Attachment.Type.AUDIO,
+ Attachment.Type.VIDEO,
+ Attachment.Type.GIFV -> shareMediaFile(directory, attachment.url)
+ else -> Log.e(TAG, "Unknown media format for sharing.")
+ }
}
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt
index ff87f978..dec4586b 100644
--- a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt
@@ -1,13 +1,19 @@
package com.keylesspalace.tusky.adapter
+import android.graphics.Typeface
+import android.text.SpannableStringBuilder
+import android.text.Spanned
+import android.text.style.StyleSpan
import android.view.View
-import androidx.core.text.BidiFormatter
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.interfaces.AccountActionListener
-import com.keylesspalace.tusky.util.*
+import com.keylesspalace.tusky.util.emojify
+import com.keylesspalace.tusky.util.loadAvatar
+import com.keylesspalace.tusky.util.unicodeWrap
+import com.keylesspalace.tusky.util.visible
import kotlinx.android.synthetic.main.item_follow_request_notification.view.*
internal class FollowRequestViewHolder(itemView: View, private val showHeader: Boolean) : RecyclerView.ViewHolder(itemView) {
@@ -15,13 +21,16 @@ internal class FollowRequestViewHolder(itemView: View, private val showHeader: B
private val animateAvatar: Boolean = PreferenceManager.getDefaultSharedPreferences(itemView.context)
.getBoolean("animateGifAvatars", false)
- fun setupWithAccount(account: Account, formatter: BidiFormatter?) {
+ fun setupWithAccount(account: Account) {
id = account.id
- val wrappedName = formatter?.unicodeWrap(account.name) ?: account.name
+ val wrappedName = account.name.unicodeWrap()
val emojifiedName: CharSequence = wrappedName.emojify(account.emojis, itemView)
itemView.displayNameTextView.text = emojifiedName
if (showHeader) {
- itemView.notificationTextView?.text = itemView.context.getString(R.string.notification_follow_request_format, emojifiedName)
+ val wholeMessage: String = itemView.context.getString(R.string.notification_follow_request_format, wrappedName)
+ itemView.notificationTextView?.text = SpannableStringBuilder(wholeMessage).apply {
+ setSpan(StyleSpan(Typeface.BOLD), 0, wrappedName.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+ }.emojify(account.emojis, itemView)
}
itemView.notificationTextView?.visible(showHeader)
val format = itemView.context.getString(R.string.status_username_format)
diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java
index 548d893c..dab3d4fe 100644
--- a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java
+++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java
@@ -53,7 +53,7 @@ public class FollowRequestsAdapter extends AccountAdapter {
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
- holder.setupWithAccount(accountList.get(position), null);
+ holder.setupWithAccount(accountList.get(position));
holder.setupActionListener(accountActionListener);
}
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java
index 936d0edf..dcaf4169 100644
--- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java
+++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java
@@ -35,7 +35,6 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
-import androidx.core.text.BidiFormatter;
import androidx.recyclerview.widget.RecyclerView;
import com.keylesspalace.tusky.R;
@@ -51,6 +50,7 @@ import com.keylesspalace.tusky.util.ImageLoadingHelper;
import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.SmartLengthInputFilter;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
+import com.keylesspalace.tusky.util.StringUtils;
import com.keylesspalace.tusky.util.TimestampUtils;
import com.keylesspalace.tusky.viewdata.NotificationViewData;
import com.keylesspalace.tusky.viewdata.StatusViewData;
@@ -86,7 +86,6 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
private StatusActionListener statusListener;
private NotificationActionListener notificationActionListener;
private AccountActionListener accountActionListener;
- private BidiFormatter bidiFormatter;
private AdapterDataSource dataSource;
public NotificationsAdapter(String accountId,
@@ -102,7 +101,6 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
this.statusListener = statusListener;
this.notificationActionListener = notificationActionListener;
this.accountActionListener = accountActionListener;
- bidiFormatter = BidiFormatter.getInstance();
}
@NonNull
@@ -204,7 +202,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
concreteNotificaton.getAccount().getAvatar());
}
- holder.setMessage(concreteNotificaton, statusListener, bidiFormatter);
+ holder.setMessage(concreteNotificaton, statusListener);
holder.setupButtons(notificationActionListener,
concreteNotificaton.getAccount().getId(),
concreteNotificaton.getId());
@@ -221,7 +219,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
case VIEW_TYPE_FOLLOW: {
if (payloadForHolder == null) {
FollowViewHolder holder = (FollowViewHolder) viewHolder;
- holder.setMessage(concreteNotificaton.getAccount(), bidiFormatter);
+ holder.setMessage(concreteNotificaton.getAccount());
holder.setupButtons(notificationActionListener, concreteNotificaton.getAccount().getId());
}
break;
@@ -229,7 +227,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
case VIEW_TYPE_FOLLOW_REQUEST: {
if (payloadForHolder == null) {
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
- holder.setupWithAccount(concreteNotificaton.getAccount(), bidiFormatter);
+ holder.setupWithAccount(concreteNotificaton.getAccount());
holder.setupActionListener(accountActionListener);
}
}
@@ -325,11 +323,11 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
this.statusDisplayOptions = statusDisplayOptions;
}
- void setMessage(Account account, BidiFormatter bidiFormatter) {
+ void setMessage(Account account) {
Context context = message.getContext();
String format = context.getString(R.string.notification_follow_format);
- String wrappedDisplayName = bidiFormatter.unicodeWrap(account.getName());
+ String wrappedDisplayName = StringUtils.unicodeWrap(account.getName());
String wholeMessage = String.format(format, wrappedDisplayName);
CharSequence emojifiedMessage = CustomEmojiHelper.emojify(wholeMessage, account.getEmojis(), message);
message.setText(emojifiedMessage);
@@ -459,10 +457,10 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
}
}
- void setMessage(NotificationViewData.Concrete notificationViewData, LinkListener listener, BidiFormatter bidiFormatter) {
+ void setMessage(NotificationViewData.Concrete notificationViewData, LinkListener listener) {
this.statusViewData = notificationViewData.getStatusViewData();
- String displayName = bidiFormatter.unicodeWrap(notificationViewData.getAccount().getName());
+ String displayName = StringUtils.unicodeWrap(notificationViewData.getAccount().getName());
Notification.Type type = notificationViewData.getType();
Context context = message.getContext();
diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt
index 8da82d22..a990d326 100644
--- a/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt
@@ -37,18 +37,21 @@ class PollAdapter: RecyclerView.Adapter() {
private var votersCount: Int? = null
private var mode = RESULT
private var emojis: List = emptyList()
+ private var resultClickListener: View.OnClickListener? = null
fun setup(
options: List,
voteCount: Int,
votersCount: Int?,
emojis: List,
- mode: Int) {
+ mode: Int,
+ resultClickListener: View.OnClickListener?) {
this.pollOptions = options
this.voteCount = voteCount
this.votersCount = votersCount
this.emojis = emojis
this.mode = mode
+ this.resultClickListener = resultClickListener
notifyDataSetChanged()
}
@@ -84,7 +87,7 @@ class PollAdapter: RecyclerView.Adapter() {
val level = percent * 100
holder.resultTextView.background.level = level
-
+ holder.resultTextView.setOnClickListener(resultClickListener)
}
SINGLE -> {
val emojifiedPollOptionText = option.title.emojify(emojis, holder.radioButton)
diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java
index c159138b..3869ca02 100644
--- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java
+++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java
@@ -26,10 +26,12 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
+import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners;
import com.google.android.material.button.MaterialButton;
import com.keylesspalace.tusky.R;
+import com.keylesspalace.tusky.ViewMediaActivity;
import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.entity.Attachment.Focus;
import com.keylesspalace.tusky.entity.Attachment.MetaData;
@@ -615,7 +617,13 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
final String accountId,
final String statusContent,
StatusDisplayOptions statusDisplayOptions) {
- avatar.setOnClickListener(v -> listener.onViewAccount(accountId));
+ View.OnClickListener profileButtonClickListener = button -> {
+ listener.onViewAccount(accountId);
+ };
+
+ avatar.setOnClickListener(profileButtonClickListener);
+ displayName.setOnClickListener(profileButtonClickListener);
+
replyButton.setOnClickListener(v -> {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
@@ -734,7 +742,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
}
if (cardView != null) {
- setupCard(status, statusDisplayOptions.cardViewMode());
+ setupCard(status, statusDisplayOptions.cardViewMode(), statusDisplayOptions);
}
setupButtons(listener, status.getSenderId(), status.getContent().toString(),
@@ -915,12 +923,18 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
if (expired || poll.getVoted()) {
// no voting possible
- pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), poll.getVotersCount(), emojis, PollAdapter.RESULT);
+ View.OnClickListener viewThreadListener = v -> {
+ int position = getAdapterPosition();
+ if (position != RecyclerView.NO_POSITION) {
+ listener.onViewThread(position);
+ }
+ };
+ pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), poll.getVotersCount(), emojis, PollAdapter.RESULT, viewThreadListener);
pollButton.setVisibility(View.GONE);
} else {
// voting possible
- pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), poll.getVotersCount(), emojis, poll.getMultiple() ? PollAdapter.MULTIPLE : PollAdapter.SINGLE);
+ pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), poll.getVotersCount(), emojis, poll.getMultiple() ? PollAdapter.MULTIPLE : PollAdapter.SINGLE, null);
pollButton.setVisibility(View.VISIBLE);
@@ -964,15 +978,14 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
if (statusDisplayOptions.useAbsoluteTime()) {
pollDurationInfo = context.getString(R.string.poll_info_time_absolute, getAbsoluteTime(poll.getExpiresAt()));
} else {
- String pollDuration = TimestampUtils.formatPollDuration(pollDescription.getContext(), poll.getExpiresAt().getTime(), timestamp);
- pollDurationInfo = context.getString(R.string.poll_info_time_relative, pollDuration);
+ pollDurationInfo = TimestampUtils.formatPollDuration(pollDescription.getContext(), poll.getExpiresAt().getTime(), timestamp);
}
}
return pollDescription.getContext().getString(R.string.poll_info_format, votesText, pollDurationInfo);
}
- protected void setupCard(StatusViewData.Concrete status, CardViewMode cardViewMode) {
+ protected void setupCard(StatusViewData.Concrete status, CardViewMode cardViewMode, StatusDisplayOptions statusDisplayOptions) {
if (cardViewMode != CardViewMode.NONE &&
status.getAttachments().size() == 0 &&
status.getCard() != null &&
@@ -994,7 +1007,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
cardUrl.setText(card.getUrl());
- if (!TextUtils.isEmpty(card.getImage())) {
+ // Statuses from other activitypub sources can be marked sensitive even if there's no media,
+ // so let's blur the preview in that case
+ // If media previews are disabled, show placeholder for cards as well
+ if (statusDisplayOptions.mediaPreviewEnabled() && !status.isSensitive() && !TextUtils.isEmpty(card.getImage())) {
int topLeftRadius = 0;
int topRightRadius = 0;
@@ -1025,12 +1041,29 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
bottomLeftRadius = radius;
}
+ RequestBuilder builder = Glide.with(cardImage).load(card.getImage());
+ if (statusDisplayOptions.useBlurhash() && !TextUtils.isEmpty(card.getBlurhash())) {
+ builder = builder.placeholder(decodeBlurHash(card.getBlurhash()));
+ }
+ builder.transform(
+ new CenterCrop(),
+ new GranularRoundedCorners(topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius)
+ )
+ .into(cardImage);
+ } else if (statusDisplayOptions.useBlurhash() && !TextUtils.isEmpty(card.getBlurhash())) {
+ int radius = cardImage.getContext().getResources()
+ .getDimensionPixelSize(R.dimen.card_radius);
- Glide.with(cardImage)
- .load(card.getImage())
+ cardView.setOrientation(LinearLayout.HORIZONTAL);
+ cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
+ cardImage.getLayoutParams().width = cardImage.getContext().getResources()
+ .getDimensionPixelSize(R.dimen.card_image_horizontal_width);
+ cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
+ cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
+ Glide.with(cardImage).load(decodeBlurHash(card.getBlurhash()))
.transform(
new CenterCrop(),
- new GranularRoundedCorners(topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius)
+ new GranularRoundedCorners(radius, 0, 0, radius)
)
.into(cardImage);
} else {
@@ -1043,7 +1076,15 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
cardImage.setImageResource(R.drawable.card_image_placeholder);
}
- cardView.setOnClickListener(v -> LinkHelper.openLink(card.getUrl(), v.getContext()));
+ View.OnClickListener visitLink = v -> LinkHelper.openLink(card.getUrl(), v.getContext());
+ View.OnClickListener openImage = v -> cardView.getContext().startActivity(ViewMediaActivity.newSingleImageIntent(cardView.getContext(), card.getEmbed_url()));
+
+ cardInfo.setOnClickListener(visitLink);
+ // View embedded photos in our image viewer instead of opening the browser
+ cardImage.setOnClickListener(card.getType().equals(Card.TYPE_PHOTO) && !TextUtils.isEmpty(card.getEmbed_url()) ?
+ openImage :
+ visitLink);
+
cardView.setClipToOutline(true);
} else {
cardView.setVisibility(View.GONE);
diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java
index 0d844b91..6350dde0 100644
--- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java
+++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java
@@ -106,7 +106,7 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
StatusDisplayOptions statusDisplayOptions,
@Nullable Object payloads) {
super.setupWithStatus(status, listener, statusDisplayOptions, payloads);
- setupCard(status, CardViewMode.FULL_WIDTH); // Always show card for detailed status
+ setupCard(status, CardViewMode.FULL_WIDTH, statusDisplayOptions); // Always show card for detailed status
if (payloads == null) {
setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener);
diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt
index 007ba546..cd877a15 100644
--- a/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt
@@ -36,8 +36,8 @@ interface ItemInteractionListener {
fun onTabRemoved(position: Int)
fun onStartDelete(viewHolder: RecyclerView.ViewHolder)
fun onStartDrag(viewHolder: RecyclerView.ViewHolder)
- fun onActionChipClicked(tab: TabData)
- fun onChipClicked(tab: TabData, chipPosition: Int)
+ fun onActionChipClicked(tab: TabData, tabPosition: Int)
+ fun onChipClicked(tab: TabData, tabPosition: Int, chipPosition: Int)
}
class TabAdapter(private var data: List,
@@ -62,16 +62,17 @@ class TabAdapter(private var data: List,
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val context = holder.itemView.context
- if (!small && data[position].id == LIST) {
- holder.itemView.textView.text = data[position].arguments.getOrNull(1).orEmpty()
+ val tab = data[position]
+ if (!small && tab.id == LIST) {
+ holder.itemView.textView.text = tab.arguments.getOrNull(1).orEmpty()
} else {
- holder.itemView.textView.setText(data[position].text)
+ holder.itemView.textView.setText(tab.text)
}
- val iconDrawable = ThemeUtils.getTintedDrawable(context, data[position].icon, android.R.attr.textColorSecondary)
+ val iconDrawable = ThemeUtils.getTintedDrawable(context, tab.icon, android.R.attr.textColorSecondary)
holder.itemView.textView.setCompoundDrawablesRelativeWithIntrinsicBounds(iconDrawable, null, null, null)
if (small) {
holder.itemView.textView.setOnClickListener {
- listener.onTabAdded(data[position])
+ listener.onTabAdded(tab)
}
}
holder.itemView.imageView?.setOnTouchListener { _, event ->
@@ -96,7 +97,7 @@ class TabAdapter(private var data: List,
if (!small) {
- if (data[position].id == HASHTAG) {
+ if (tab.id == HASHTAG) {
holder.itemView.chipGroup.show()
/*
@@ -104,34 +105,33 @@ class TabAdapter(private var data: List,
* The other dynamic chips are inserted in front of the actionChip.
* This code tries to reuse already added chips to reduce the number of Views created.
*/
- data[position].arguments.forEachIndexed { i, arg ->
+ tab.arguments.forEachIndexed { i, arg ->
val chip = holder.itemView.chipGroup.getChildAt(i).takeUnless { it.id == R.id.actionChip } as Chip?
?: Chip(context).apply {
- text = arg
holder.itemView.chipGroup.addView(this, holder.itemView.chipGroup.size - 1)
}
chip.text = arg
- if(data[position].arguments.size <= 1) {
+ if(tab.arguments.size <= 1) {
chip.chipIcon = null
chip.setOnClickListener(null)
} else {
val cancelIcon = ThemeUtils.getTintedDrawable(context, R.drawable.ic_cancel_24dp, android.R.attr.textColorPrimary)
chip.chipIcon = cancelIcon
chip.setOnClickListener {
- listener.onChipClicked(data[position], i)
+ listener.onChipClicked(tab, holder.adapterPosition, i)
}
}
}
- while(holder.itemView.chipGroup.size - 1 > data[position].arguments.size) {
- holder.itemView.chipGroup.removeViewAt(data[position].arguments.size - 1)
+ while(holder.itemView.chipGroup.size - 1 > tab.arguments.size) {
+ holder.itemView.chipGroup.removeViewAt(tab.arguments.size)
}
holder.itemView.actionChip.setOnClickListener {
- listener.onActionChipClicked(data[position])
+ listener.onActionChipClicked(tab, holder.adapterPosition)
}
} else {
@@ -140,9 +140,7 @@ class TabAdapter(private var data: List,
}
}
- override fun getItemCount(): Int {
- return data.size
- }
+ override fun getItemCount() = data.size
fun setRemoveButtonVisible(enabled: Boolean) {
if (removeButtonEnabled != enabled) {
diff --git a/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt b/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt
index 8e62faeb..288de430 100644
--- a/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt
@@ -19,4 +19,5 @@ data class ProfileEditedEvent(val newProfileData: Account) : Dispatchable
data class PreferenceChangedEvent(val preferenceKey: String) : Dispatchable
data class MainTabsChangedEvent(val newTabs: List) : Dispatchable
data class PollVoteEvent(val statusId: String, val poll: Poll) : Dispatchable
-data class DomainMuteEvent(val instance: String): Dispatchable
\ No newline at end of file
+data class DomainMuteEvent(val instance: String): Dispatchable
+data class AnnouncementReadEvent(val announcementId: String): Dispatchable
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt
new file mode 100644
index 00000000..c4fa93f2
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt
@@ -0,0 +1,115 @@
+/* Copyright 2020 Tusky Contributors
+ *
+ * This file is a part of Tusky.
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tusky; if not,
+ * see . */
+
+package com.keylesspalace.tusky.components.announcements
+
+import android.view.ContextThemeWrapper
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.core.view.size
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.chip.Chip
+import com.google.android.material.chip.ChipGroup
+import com.keylesspalace.tusky.R
+import com.keylesspalace.tusky.entity.Announcement
+import com.keylesspalace.tusky.entity.Emoji
+import com.keylesspalace.tusky.util.emojify
+import kotlinx.android.synthetic.main.item_announcement.view.*
+
+interface AnnouncementActionListener {
+ fun openReactionPicker(announcementId: String, target: View)
+ fun addReaction(announcementId: String, name: String)
+ fun removeReaction(announcementId: String, name: String)
+}
+
+class AnnouncementAdapter(
+ private var items: List = emptyList(),
+ private val listener: AnnouncementActionListener
+) : RecyclerView.Adapter() {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnnouncementViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_announcement, parent, false)
+ return AnnouncementViewHolder(view)
+ }
+
+ override fun onBindViewHolder(viewHolder: AnnouncementViewHolder, position: Int) {
+ viewHolder.bind(items[position])
+ }
+
+ override fun getItemCount() = items.size
+
+ fun updateList(items: List) {
+ this.items = items
+ notifyDataSetChanged()
+ }
+
+ inner class AnnouncementViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
+
+ private val text: TextView = view.text
+ private val chips: ChipGroup = view.chipGroup
+ private val addReactionChip: Chip = view.addReactionChip
+
+ fun bind(item: Announcement) {
+ text.text = item.content
+
+ item.reactions.forEachIndexed { i, reaction ->
+ (chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip?
+ ?: Chip(ContextThemeWrapper(view.context, R.style.Widget_MaterialComponents_Chip_Choice)).apply {
+ isCheckable = true
+ checkedIcon = null
+ chips.addView(this, i)
+ })
+ .apply {
+ val emojiText = if (reaction.url == null) {
+ reaction.name
+ } else {
+ view.context.getString(R.string.emoji_shortcode_format, reaction.name)
+ }
+ text = ("$emojiText ${reaction.count}")
+ .emojify(
+ listOf(Emoji(
+ reaction.name,
+ reaction.url ?: "",
+ reaction.staticUrl ?: "",
+ null
+ )),
+ this
+ )
+
+ isChecked = reaction.me
+
+ setOnClickListener {
+ if (reaction.me) {
+ listener.removeReaction(item.id, reaction.name)
+ } else {
+ listener.addReaction(item.id, reaction.name)
+ }
+ }
+ }
+ }
+
+ while (chips.size - 1 > item.reactions.size) {
+ chips.removeViewAt(item.reactions.size)
+ }
+
+ addReactionChip.setOnClickListener {
+ listener.openReactionPicker(item.id, it)
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt
new file mode 100644
index 00000000..5bfd17bc
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt
@@ -0,0 +1,153 @@
+/* Copyright 2020 Tusky Contributors
+ *
+ * This file is a part of Tusky.
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tusky; if not,
+ * see . */
+
+package com.keylesspalace.tusky.components.announcements
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.MenuItem
+import android.view.View
+import android.widget.PopupWindow
+import androidx.activity.viewModels
+import androidx.lifecycle.Observer
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.keylesspalace.tusky.BaseActivity
+import com.keylesspalace.tusky.R
+import com.keylesspalace.tusky.adapter.EmojiAdapter
+import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener
+import com.keylesspalace.tusky.di.Injectable
+import com.keylesspalace.tusky.di.ViewModelFactory
+import com.keylesspalace.tusky.util.*
+import com.keylesspalace.tusky.view.EmojiPicker
+import kotlinx.android.synthetic.main.activity_announcements.*
+import kotlinx.android.synthetic.main.toolbar_basic.*
+import javax.inject.Inject
+
+class AnnouncementsActivity : BaseActivity(), AnnouncementActionListener, OnEmojiSelectedListener, Injectable {
+
+ @Inject
+ lateinit var viewModelFactory: ViewModelFactory
+
+ private val viewModel: AnnouncementsViewModel by viewModels { viewModelFactory }
+
+ private val adapter = AnnouncementAdapter(emptyList(), this)
+
+ private val picker by lazy { EmojiPicker(this) }
+ private val pickerDialog by lazy {
+ PopupWindow(this)
+ .apply {
+ contentView = picker
+ isFocusable = true
+ setOnDismissListener {
+ currentAnnouncementId = null
+ }
+ }
+ }
+ private var currentAnnouncementId: String? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_announcements)
+
+ setSupportActionBar(toolbar)
+ supportActionBar?.apply {
+ title = getString(R.string.title_announcements)
+ setDisplayHomeAsUpEnabled(true)
+ setDisplayShowHomeEnabled(true)
+ }
+
+ swipeRefreshLayout.setOnRefreshListener(this::refreshAnnouncements)
+ swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
+
+ announcementsList.setHasFixedSize(true)
+ announcementsList.layoutManager = LinearLayoutManager(this)
+ val divider = DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
+ announcementsList.addItemDecoration(divider)
+ announcementsList.adapter = adapter
+
+ viewModel.announcements.observe(this) {
+ when (it) {
+ is Success -> {
+ progressBar.hide()
+ swipeRefreshLayout.isRefreshing = false
+ if (it.data.isNullOrEmpty()) {
+ errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_announcements)
+ errorMessageView.show()
+ } else {
+ errorMessageView.hide()
+ }
+ adapter.updateList(it.data ?: listOf())
+ }
+ is Loading -> {
+ errorMessageView.hide()
+ }
+ is Error -> {
+ progressBar.hide()
+ swipeRefreshLayout.isRefreshing = false
+ errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) {
+ refreshAnnouncements()
+ }
+ errorMessageView.show()
+ }
+ }
+ }
+
+ viewModel.emojis.observe(this) {
+ picker.adapter = EmojiAdapter(it, this)
+ }
+
+ viewModel.load()
+ progressBar.show()
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ android.R.id.home -> {
+ onBackPressed()
+ return true
+ }
+ }
+ return super.onOptionsItemSelected(item)
+ }
+
+ private fun refreshAnnouncements() {
+ viewModel.load()
+ swipeRefreshLayout.isRefreshing = true
+ }
+
+ override fun openReactionPicker(announcementId: String, target: View) {
+ currentAnnouncementId = announcementId
+ pickerDialog.showAsDropDown(target)
+ }
+
+ override fun onEmojiSelected(shortcode: String) {
+ viewModel.addReaction(currentAnnouncementId!!, shortcode)
+ pickerDialog.dismiss()
+ }
+
+ override fun addReaction(announcementId: String, name: String) {
+ viewModel.addReaction(announcementId, name)
+ }
+
+ override fun removeReaction(announcementId: String, name: String) {
+ viewModel.removeReaction(announcementId, name)
+ }
+
+ companion object {
+ fun newIntent(context: Context) = Intent(context, AnnouncementsActivity::class.java)
+ }
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt
new file mode 100644
index 00000000..964cc739
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt
@@ -0,0 +1,187 @@
+/* Copyright 2020 Tusky Contributors
+ *
+ * This file is a part of Tusky.
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tusky; if not,
+ * see . */
+
+package com.keylesspalace.tusky.components.announcements
+
+import android.util.Log
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.keylesspalace.tusky.appstore.AnnouncementReadEvent
+import com.keylesspalace.tusky.appstore.EventHub
+import com.keylesspalace.tusky.db.AccountManager
+import com.keylesspalace.tusky.db.AppDatabase
+import com.keylesspalace.tusky.db.InstanceEntity
+import com.keylesspalace.tusky.entity.Announcement
+import com.keylesspalace.tusky.entity.Emoji
+import com.keylesspalace.tusky.entity.Instance
+import com.keylesspalace.tusky.network.MastodonApi
+import com.keylesspalace.tusky.util.*
+import io.reactivex.rxkotlin.Singles
+import javax.inject.Inject
+
+class AnnouncementsViewModel @Inject constructor(
+ accountManager: AccountManager,
+ private val appDatabase: AppDatabase,
+ private val mastodonApi: MastodonApi,
+ private val eventHub: EventHub
+) : RxAwareViewModel() {
+
+ private val announcementsMutable = MutableLiveData>>()
+ val announcements: LiveData>> = announcementsMutable
+
+ private val emojisMutable = MutableLiveData>()
+ val emojis: LiveData> = emojisMutable
+
+ init {
+ Singles.zip(
+ mastodonApi.getCustomEmojis(),
+ appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
+ .map> { Either.Left(it) }
+ .onErrorResumeNext(
+ mastodonApi.getInstance()
+ .map { Either.Right(it) }
+ )
+ ) { emojis, either ->
+ either.asLeftOrNull()?.copy(emojiList = emojis)
+ ?: InstanceEntity(
+ accountManager.activeAccount?.domain!!,
+ emojis,
+ either.asRight().maxTootChars,
+ either.asRight().pollLimits?.maxOptions,
+ either.asRight().pollLimits?.maxOptionChars,
+ either.asRight().version
+ )
+ }
+ .doOnSuccess {
+ appDatabase.instanceDao().insertOrReplace(it)
+ }
+ .subscribe({
+ emojisMutable.postValue(it.emojiList)
+ }, {
+ Log.w(TAG, "Failed to get custom emojis.", it)
+ })
+ .autoDispose()
+ }
+
+ fun load() {
+ announcementsMutable.postValue(Loading())
+ mastodonApi.listAnnouncements()
+ .subscribe({
+ announcementsMutable.postValue(Success(it))
+ it.filter { announcement -> !announcement.read }
+ .forEach { announcement ->
+ mastodonApi.dismissAnnouncement(announcement.id)
+ .subscribe(
+ {
+ eventHub.dispatch(AnnouncementReadEvent(announcement.id))
+ },
+ { throwable ->
+ Log.d(TAG, "Failed to mark announcement as read.", throwable)
+ }
+ )
+ .autoDispose()
+ }
+ }, {
+ announcementsMutable.postValue(Error(cause = it))
+ })
+ .autoDispose()
+ }
+
+ fun addReaction(announcementId: String, name: String) {
+ mastodonApi.addAnnouncementReaction(announcementId, name)
+ .subscribe({
+ announcementsMutable.postValue(
+ Success(
+ announcements.value!!.data!!.map { announcement ->
+ if (announcement.id == announcementId) {
+ announcement.copy(
+ reactions = if (announcement.reactions.find { reaction -> reaction.name == name } != null) {
+ announcement.reactions.map { reaction ->
+ if (reaction.name == name) {
+ reaction.copy(
+ count = reaction.count + 1,
+ me = true
+ )
+ } else {
+ reaction
+ }
+ }
+ } else {
+ listOf(
+ *announcement.reactions.toTypedArray(),
+ emojis.value!!.find { emoji -> emoji.shortcode == name }
+ !!.run {
+ Announcement.Reaction(
+ name,
+ 1,
+ true,
+ url,
+ staticUrl
+ )
+ }
+ )
+ }
+ )
+ } else {
+ announcement
+ }
+ }
+ )
+ )
+ }, {
+ Log.w(TAG, "Failed to add reaction to the announcement.", it)
+ })
+ .autoDispose()
+ }
+
+ fun removeReaction(announcementId: String, name: String) {
+ mastodonApi.removeAnnouncementReaction(announcementId, name)
+ .subscribe({
+ announcementsMutable.postValue(
+ Success(
+ announcements.value!!.data!!.map { announcement ->
+ if (announcement.id == announcementId) {
+ announcement.copy(
+ reactions = announcement.reactions.mapNotNull { reaction ->
+ if (reaction.name == name) {
+ if (reaction.count > 1) {
+ reaction.copy(
+ count = reaction.count - 1,
+ me = false
+ )
+ } else {
+ null
+ }
+ } else {
+ reaction
+ }
+ }
+ )
+ } else {
+ announcement
+ }
+ }
+ )
+ )
+ }, {
+ Log.w(TAG, "Failed to remove reaction from the announcement.", it)
+ })
+ .autoDispose()
+ }
+
+ companion object {
+ private const val TAG = "AnnouncementsViewModel"
+ }
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt
index 6b0f5745..4eba7e32 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt
@@ -49,9 +49,7 @@ import androidx.core.view.inputmethod.InputConnectionCompat
import androidx.core.view.inputmethod.InputContentInfoCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
-import androidx.lifecycle.Observer
import androidx.preference.PreferenceManager
-import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.transition.TransitionManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
@@ -150,7 +148,7 @@ class ComposeActivity : BaseActivity(),
/* If the composer is started up as a reply to another post, override the "starting" state
* based on what the intent from the reply request passes. */
if (intent != null) {
- this.composeOptions = intent.getParcelableExtra(COMPOSE_OPTIONS_EXTRA)
+ this.composeOptions = intent.getParcelableExtra(COMPOSE_OPTIONS_EXTRA)
viewModel.setup(composeOptions)
setupReplyViews(composeOptions?.replyingStatusAuthor)
val tootText = composeOptions?.tootText
@@ -302,7 +300,7 @@ class ComposeActivity : BaseActivity(),
}
viewModel.media.observe { media ->
mediaAdapter.submitList(media)
- if(media.size != mediaCount) {
+ if (media.size != mediaCount) {
mediaCount = media.size
composeMediaPreviewBar.visible(media.isNotEmpty())
updateSensitiveMediaToggle(viewModel.markMediaAsSensitive.value != false, viewModel.showContentWarning.value != false)
@@ -312,8 +310,8 @@ class ComposeActivity : BaseActivity(),
pollPreview.visible(poll != null)
poll?.let(pollPreview::setPoll)
}
- viewModel.scheduledAt.observe {scheduledAt ->
- if(scheduledAt == null) {
+ viewModel.scheduledAt.observe { scheduledAt ->
+ if (scheduledAt == null) {
composeScheduleView.resetSchedule()
} else {
composeScheduleView.setDateTime(scheduledAt)
@@ -345,7 +343,6 @@ class ComposeActivity : BaseActivity(),
scheduleBehavior = BottomSheetBehavior.from(composeScheduleView)
emojiBehavior = BottomSheetBehavior.from(emojiView)
- emojiView.layoutManager = GridLayoutManager(this, 3, GridLayoutManager.HORIZONTAL, false)
enableButton(composeEmojiButton, clickable = false, colorActive = false)
// Setup the interface buttons.
@@ -553,7 +550,7 @@ class ComposeActivity : BaseActivity(),
}
private fun onScheduleClick() {
- if(viewModel.scheduledAt.value == null) {
+ if (viewModel.scheduledAt.value == null) {
composeScheduleView.openPickDateDialog()
} else {
showScheduleView()
@@ -682,7 +679,15 @@ class ComposeActivity : BaseActivity(),
}
private fun updateVisibleCharactersLeft() {
- composeCharactersLeftView.text = String.format(Locale.getDefault(), "%d", maximumTootCharacters - calculateTextLength())
+ val remainingLength = maximumTootCharacters - calculateTextLength()
+ composeCharactersLeftView.text = String.format(Locale.getDefault(), "%d", remainingLength)
+
+ val textColor = if (remainingLength < 0) {
+ ContextCompat.getColor(this, R.color.tusky_red)
+ } else {
+ ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
+ }
+ composeCharactersLeftView.setTextColor(textColor)
}
private fun onContentWarningChanged() {
@@ -708,9 +713,9 @@ class ComposeActivity : BaseActivity(),
// Verify the returned content's type is of the correct MIME type
val supported = inputContentInfo.description.hasMimeType("image/*")
- if(supported) {
+ if (supported) {
val lacksPermission = (flags and InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0
- if(lacksPermission) {
+ if (lacksPermission) {
try {
inputContentInfo.requestPermission()
} catch (e: Exception) {
@@ -737,11 +742,13 @@ class ComposeActivity : BaseActivity(),
composeEditField.error = getString(R.string.error_empty)
enableButtons(true)
} else if (characterCount <= maximumTootCharacters) {
- finishingUploadDialog = ProgressDialog.show(
- this, getString(R.string.dialog_title_finishing_media_upload),
- getString(R.string.dialog_message_uploading_media), true, true)
+ if (viewModel.media.value!!.isNotEmpty()) {
+ finishingUploadDialog = ProgressDialog.show(
+ this, getString(R.string.dialog_title_finishing_media_upload),
+ getString(R.string.dialog_message_uploading_media), true, true)
+ }
- viewModel.sendStatus(contentText, spoilerText).observe(this, Observer {
+ viewModel.sendStatus(contentText, spoilerText).observe(this, {
finishingUploadDialog?.dismiss()
deleteDraftAndFinish()
})
@@ -762,7 +769,7 @@ class ComposeActivity : BaseActivity(),
Snackbar.LENGTH_SHORT).apply {
}
- bar.setAction(R.string.action_retry) { onMediaPick()}
+ bar.setAction(R.string.action_retry) { onMediaPick() }
//necessary so snackbar is shown over everything
bar.view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation)
bar.show()
@@ -904,7 +911,7 @@ class ComposeActivity : BaseActivity(),
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
Log.d(TAG, event.toString())
- if(event.action == KeyEvent.ACTION_DOWN) {
+ if (event.action == KeyEvent.ACTION_DOWN) {
if (event.isCtrlPressed) {
if (keyCode == KeyEvent.KEYCODE_ENTER) {
// send toot by pressing CTRL + ENTER
@@ -994,6 +1001,7 @@ class ComposeActivity : BaseActivity(),
@Parcelize
data class ComposeOptions(
// Let's keep fields var until all consumers are Kotlin
+ var scheduledTootUid: String? = null,
var savedTootUid: Int? = null,
var tootText: String? = null,
var mediaUrls: List? = null,
@@ -1008,7 +1016,8 @@ class ComposeActivity : BaseActivity(),
var mediaAttachments: List? = null,
var scheduledAt: String? = null,
var sensitive: Boolean? = null,
- var poll: NewPoll? = null
+ var poll: NewPoll? = null,
+ var modifiedInitialState: Boolean? = null
) : Parcelable
companion object {
@@ -1017,7 +1026,7 @@ class ComposeActivity : BaseActivity(),
private const val MEDIA_TAKE_PHOTO_RESULT = 2
private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1
- private const val COMPOSE_OPTIONS_EXTRA = "COMPOSE_OPTIONS"
+ internal const val COMPOSE_OPTIONS_EXTRA = "COMPOSE_OPTIONS"
private const val PHOTO_UPLOAD_URI_KEY = "PHOTO_UPLOAD_URI"
// Mastodon only counts URLs as this long in terms of status character limits
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt
index d38fe94b..2c015899 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt
@@ -33,6 +33,7 @@ import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.service.ServiceClient
import com.keylesspalace.tusky.service.TootToSend
import com.keylesspalace.tusky.util.*
+import io.reactivex.Observable.just
import io.reactivex.disposables.Disposable
import io.reactivex.rxkotlin.Singles
import java.util.*
@@ -58,11 +59,13 @@ class ComposeViewModel
private var replyingStatusContent: String? = null
internal var startingText: String? = null
private var savedTootUid: Int = 0
+ private var scheduledTootUid: String? = null
private var startingContentWarning: String = ""
private var inReplyToId: String? = null
private var startingVisibility: Status.Visibility = Status.Visibility.UNKNOWN
private var contentWarningStateChanged: Boolean = false
+ private var modifiedInitialState: Boolean = false
private val instance: MutableLiveData = MutableLiveData(null)
@@ -93,6 +96,7 @@ class ComposeViewModel
private val mediaToDisposable = mutableMapOf()
+ private val isEditingScheduledToot get() = !scheduledTootUid.isNullOrEmpty()
init {
@@ -197,7 +201,7 @@ class ComposeViewModel
val mediaChanged = !media.value.isNullOrEmpty()
val pollChanged = poll.value != null
- return textChanged || contentWarningChanged || mediaChanged || pollChanged
+ return modifiedInitialState || textChanged || contentWarningChanged || mediaChanged || pollChanged
}
fun contentWarningChanged(value: Boolean) {
@@ -240,7 +244,14 @@ class ComposeViewModel
content: String,
spoilerText: String
): LiveData {
- return media
+
+ val deletionObservable = if (isEditingScheduledToot) {
+ api.deleteScheduledStatus(scheduledTootUid.toString()).toObservable().map { Unit }
+ } else {
+ just(Unit)
+ }.toLiveData()
+
+ val sendObservable = media
.filter { items -> items.all { it.uploadPercent == -1 } }
.map {
val mediaIds = ArrayList()
@@ -271,8 +282,13 @@ class ComposeViewModel
idempotencyKey = randomAlphanumericString(16),
retries = 0
)
+
serviceClient.sendToot(tootToSend)
}
+
+ return combineLiveData(deletionObservable, sendObservable) { _, _ -> Unit }
+
+
}
fun updateDescription(localId: Long, description: String): LiveData {
@@ -369,7 +385,7 @@ class ComposeViewModel
preferredVisibility.num.coerceAtLeast(replyVisibility.num))
inReplyToId = composeOptions?.inReplyToId
-
+ modifiedInitialState = composeOptions?.modifiedInitialState == true
val contentWarning = composeOptions?.contentWarning
if (contentWarning != null) {
@@ -405,6 +421,7 @@ class ComposeViewModel
savedTootUid = composeOptions?.savedTootUid ?: 0
+ scheduledTootUid = composeOptions?.scheduledTootUid
startingText = composeOptions?.tootText
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt
index dc2d60c8..b1ab32d7 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt
@@ -21,8 +21,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.viewModels
-import androidx.lifecycle.Observer
-import androidx.paging.PagedList
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
@@ -36,7 +34,10 @@ import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.fragment.SFragment
import com.keylesspalace.tusky.interfaces.ReselectableFragment
import com.keylesspalace.tusky.interfaces.StatusActionListener
-import com.keylesspalace.tusky.util.*
+import com.keylesspalace.tusky.util.CardViewMode
+import com.keylesspalace.tusky.util.NetworkState
+import com.keylesspalace.tusky.util.StatusDisplayOptions
+import com.keylesspalace.tusky.util.hide
import kotlinx.android.synthetic.main.fragment_timeline.*
import javax.inject.Inject
@@ -83,21 +84,21 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
initSwipeToRefresh()
- viewModel.conversations.observe(viewLifecycleOwner, Observer> {
+ viewModel.conversations.observe(viewLifecycleOwner) {
adapter.submitList(it)
- })
- viewModel.networkState.observe(viewLifecycleOwner, Observer {
+ }
+ viewModel.networkState.observe(viewLifecycleOwner) {
adapter.setNetworkState(it)
- })
+ }
viewModel.load()
}
private fun initSwipeToRefresh() {
- viewModel.refreshState.observe(viewLifecycleOwner, Observer {
+ viewModel.refreshState.observe(viewLifecycleOwner) {
swipeRefreshLayout.isRefreshing = it == NetworkState.LOADING
- })
+ }
swipeRefreshLayout.setOnRefreshListener {
viewModel.refresh()
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationFetcher.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationFetcher.kt
new file mode 100644
index 00000000..394a6846
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationFetcher.kt
@@ -0,0 +1,82 @@
+package com.keylesspalace.tusky.components.notifications
+
+import android.util.Log
+import com.keylesspalace.tusky.db.AccountEntity
+import com.keylesspalace.tusky.db.AccountManager
+import com.keylesspalace.tusky.entity.Marker
+import com.keylesspalace.tusky.entity.Notification
+import com.keylesspalace.tusky.network.MastodonApi
+import com.keylesspalace.tusky.util.isLessThan
+import javax.inject.Inject
+
+class NotificationFetcher @Inject constructor(
+ private val mastodonApi: MastodonApi,
+ private val accountManager: AccountManager,
+ private val notifier: Notifier
+) {
+ fun fetchAndShow() {
+ for (account in accountManager.getAllAccountsOrderedByActive()) {
+ if (account.notificationsEnabled) {
+ try {
+ val notifications = fetchNotifications(account)
+ notifications.forEachIndexed { index, notification ->
+ notifier.show(notification, account, index == 0)
+ }
+ accountManager.saveAccount(account)
+ } catch (e: Exception) {
+ Log.w(TAG, "Error while fetching notifications", e)
+ }
+ }
+ }
+ }
+
+ private fun fetchNotifications(account: AccountEntity): MutableList {
+ val authHeader = String.format("Bearer %s", account.accessToken)
+ // We fetch marker to not load/show notifications which user has already seen
+ val marker = fetchMarker(authHeader, account)
+ if (marker != null && account.lastNotificationId.isLessThan(marker.lastReadId)) {
+ account.lastNotificationId = marker.lastReadId
+ }
+ Log.d(TAG, "getting Notifications for " + account.fullName)
+ val notifications = mastodonApi.notificationsWithAuth(
+ authHeader,
+ account.domain,
+ account.lastNotificationId
+ ).blockingGet()
+
+ val newId = account.lastNotificationId
+ var newestId = ""
+ val result = mutableListOf()
+ for (notification in notifications.reversed()) {
+ val currentId = notification.id
+ if (newestId.isLessThan(currentId)) {
+ newestId = currentId
+ account.lastNotificationId = currentId
+ }
+ if (newId.isLessThan(currentId)) {
+ result.add(notification)
+ }
+ }
+ return result
+ }
+
+ private fun fetchMarker(authHeader: String, account: AccountEntity): Marker? {
+ return try {
+ val allMarkers = mastodonApi.markersWithAuth(
+ authHeader,
+ account.domain,
+ listOf("notifications")
+ ).blockingGet()
+ val notificationMarker = allMarkers["notifications"]
+ Log.d(TAG, "Fetched marker: $notificationMarker")
+ notificationMarker
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to fetch marker", e)
+ null
+ }
+ }
+
+ companion object {
+ const val TAG = "NotificationFetcher"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java
index a094750e..3020f207 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java
+++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java
@@ -37,7 +37,6 @@ import androidx.core.app.NotificationManagerCompat;
import androidx.core.app.RemoteInput;
import androidx.core.app.TaskStackBuilder;
import androidx.core.content.ContextCompat;
-import androidx.core.text.BidiFormatter;
import androidx.work.Constraints;
import androidx.work.NetworkType;
import androidx.work.PeriodicWorkRequest;
@@ -58,6 +57,7 @@ import com.keylesspalace.tusky.entity.PollOption;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.receiver.NotificationClearBroadcastReceiver;
import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver;
+import com.keylesspalace.tusky.util.StringUtils;
import com.keylesspalace.tusky.viewdata.PollViewDataKt;
import org.json.JSONArray;
@@ -145,7 +145,6 @@ public class NotificationHelper {
String rawCurrentNotifications = account.getActiveNotifications();
JSONArray currentNotifications;
- BidiFormatter bidiFormatter = BidiFormatter.getInstance();
try {
currentNotifications = new JSONArray(rawCurrentNotifications);
@@ -174,7 +173,7 @@ public class NotificationHelper {
notificationId++;
- builder.setContentTitle(titleForType(context, body, bidiFormatter, account))
+ builder.setContentTitle(titleForType(context, body, account))
.setContentText(bodyForType(body, context));
if (body.getType() == Notification.Type.MENTION || body.getType() == Notification.Type.POLL) {
@@ -243,7 +242,7 @@ public class NotificationHelper {
if (currentNotifications.length() != 1) {
try {
String title = context.getString(R.string.notification_title_summary, currentNotifications.length());
- String text = joinNames(context, currentNotifications, bidiFormatter);
+ String text = joinNames(context, currentNotifications);
summaryBuilder.setContentTitle(title)
.setContentText(text);
} catch (JSONException e) {
@@ -573,36 +572,36 @@ public class NotificationHelper {
}
}
- private static String wrapItemAt(JSONArray array, int index, BidiFormatter bidiFormatter) throws JSONException {
- return bidiFormatter.unicodeWrap(array.get(index).toString());
+ private static String wrapItemAt(JSONArray array, int index) throws JSONException {
+ return StringUtils.unicodeWrap(array.get(index).toString());
}
@Nullable
- private static String joinNames(Context context, JSONArray array, BidiFormatter bidiFormatter) throws JSONException {
+ private static String joinNames(Context context, JSONArray array) throws JSONException {
if (array.length() > 3) {
int length = array.length();
return String.format(context.getString(R.string.notification_summary_large),
- wrapItemAt(array, length - 1, bidiFormatter),
- wrapItemAt(array, length - 2, bidiFormatter),
- wrapItemAt(array, length - 3, bidiFormatter),
+ wrapItemAt(array, length - 1),
+ wrapItemAt(array, length - 2),
+ wrapItemAt(array, length - 3),
length - 3);
} else if (array.length() == 3) {
return String.format(context.getString(R.string.notification_summary_medium),
- wrapItemAt(array, 2, bidiFormatter),
- wrapItemAt(array, 1, bidiFormatter),
- wrapItemAt(array, 0, bidiFormatter));
+ wrapItemAt(array, 2),
+ wrapItemAt(array, 1),
+ wrapItemAt(array, 0));
} else if (array.length() == 2) {
return String.format(context.getString(R.string.notification_summary_small),
- wrapItemAt(array, 1, bidiFormatter),
- wrapItemAt(array, 0, bidiFormatter));
+ wrapItemAt(array, 1),
+ wrapItemAt(array, 0));
}
return null;
}
@Nullable
- private static String titleForType(Context context, Notification notification, BidiFormatter bidiFormatter, AccountEntity account) {
- String accountName = bidiFormatter.unicodeWrap(notification.getAccount().getName());
+ private static String titleForType(Context context, Notification notification, AccountEntity account) {
+ String accountName = StringUtils.unicodeWrap(notification.getAccount().getName());
switch (notification.getType()) {
case MENTION:
return String.format(context.getString(R.string.notification_mention_format),
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationWorker.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationWorker.kt
index 9abcd3ac..ae7d4d3f 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationWorker.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationWorker.kt
@@ -16,82 +16,35 @@
package com.keylesspalace.tusky.components.notifications
import android.content.Context
-import android.util.Log
import androidx.work.ListenableWorker
import androidx.work.Worker
import androidx.work.WorkerFactory
import androidx.work.WorkerParameters
-import com.keylesspalace.tusky.db.AccountEntity
-import com.keylesspalace.tusky.db.AccountManager
-import com.keylesspalace.tusky.entity.Notification
-import com.keylesspalace.tusky.network.MastodonApi
-import com.keylesspalace.tusky.util.isLessThan
-import java.io.IOException
import javax.inject.Inject
class NotificationWorker(
- private val context: Context,
+ context: Context,
params: WorkerParameters,
- private val mastodonApi: MastodonApi,
- private val accountManager: AccountManager
+ private val notificationsFetcher: NotificationFetcher
) : Worker(context, params) {
override fun doWork(): Result {
- val accountList = accountManager.getAllAccountsOrderedByActive()
- for (account in accountList) {
- if (account.notificationsEnabled) {
- try {
- Log.d(TAG, "getting Notifications for " + account.fullName)
- val notificationsResponse = mastodonApi.notificationsWithAuth(
- String.format("Bearer %s", account.accessToken),
- account.domain
- ).execute()
- val notifications = notificationsResponse.body()
- if (notificationsResponse.isSuccessful && notifications != null) {
- onNotificationsReceived(account, notifications)
- } else {
- Log.w(TAG, "error receiving notifications")
- }
- } catch (e: IOException) {
- Log.w(TAG, "error receiving notifications", e)
- }
- }
- }
+ notificationsFetcher.fetchAndShow()
return Result.success()
}
-
- private fun onNotificationsReceived(account: AccountEntity, notificationList: List) {
- val newId = account.lastNotificationId
- var newestId = ""
- var isFirstOfBatch = true
- notificationList.reversed().forEach { notification ->
- val currentId = notification.id
- if (newestId.isLessThan(currentId)) {
- newestId = currentId
- }
- if (newId.isLessThan(currentId)) {
- NotificationHelper.make(context, notification, account, isFirstOfBatch)
- isFirstOfBatch = false
- }
- }
- account.lastNotificationId = newestId
- accountManager.saveAccount(account)
- }
-
- companion object {
- private const val TAG = "NotificationWorker"
- }
-
}
class NotificationWorkerFactory @Inject constructor(
- val api: MastodonApi,
- val accountManager: AccountManager
-): WorkerFactory() {
+ private val notificationsFetcher: NotificationFetcher
+) : WorkerFactory() {
- override fun createWorker(appContext: Context, workerClassName: String, workerParameters: WorkerParameters): ListenableWorker? {
- if(workerClassName == NotificationWorker::class.java.name) {
- return NotificationWorker(appContext, workerParameters, api, accountManager)
+ override fun createWorker(
+ appContext: Context,
+ workerClassName: String,
+ workerParameters: WorkerParameters
+ ): ListenableWorker? {
+ if (workerClassName == NotificationWorker::class.java.name) {
+ return NotificationWorker(appContext, workerParameters, notificationsFetcher)
}
return null
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/Notifier.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/Notifier.kt
new file mode 100644
index 00000000..35c33a9b
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/Notifier.kt
@@ -0,0 +1,20 @@
+package com.keylesspalace.tusky.components.notifications
+
+import android.content.Context
+import com.keylesspalace.tusky.db.AccountEntity
+import com.keylesspalace.tusky.entity.Notification
+
+/**
+ * Shows notifications.
+ */
+interface Notifier {
+ fun show(notification: Notification, account: AccountEntity, isFirstInBatch: Boolean)
+}
+
+class SystemNotifier(
+ private val context: Context
+) : Notifier {
+ override fun show(notification: Notification, account: AccountEntity, isFirstInBatch: Boolean) {
+ NotificationHelper.make(context, notification, account, isFirstInBatch)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/AccountPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt
similarity index 99%
rename from app/src/main/java/com/keylesspalace/tusky/fragment/preference/AccountPreferencesFragment.kt
rename to app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt
index 24049aa9..af882977 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/AccountPreferencesFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt
@@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see . */
-package com.keylesspalace.tusky.fragment.preference
+package com.keylesspalace.tusky.components.preference
import android.content.Intent
import android.graphics.drawable.Drawable
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt
new file mode 100644
index 00000000..c0e538a9
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt
@@ -0,0 +1,258 @@
+package com.keylesspalace.tusky.components.preference
+
+import android.app.AlarmManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.*
+import androidx.appcompat.app.AlertDialog
+import androidx.preference.Preference
+import androidx.preference.PreferenceManager
+import com.keylesspalace.tusky.R
+import com.keylesspalace.tusky.SplashActivity
+import com.keylesspalace.tusky.util.EmojiCompatFont
+import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.FONTS
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.Disposable
+import okhttp3.OkHttpClient
+import kotlin.system.exitProcess
+
+/**
+ * This Preference lets the user select their preferred emoji font
+ */
+class EmojiPreference(
+ context: Context,
+ private val okHttpClient: OkHttpClient
+) : Preference(context) {
+
+ private lateinit var selected: EmojiCompatFont
+ private lateinit var original: EmojiCompatFont
+ private val radioButtons = mutableListOf()
+ private var updated = false
+ private var currentNeedsUpdate = false
+
+ private val downloadDisposables = MutableList(FONTS.size) { null }
+
+ override fun onAttachedToHierarchy(preferenceManager: PreferenceManager) {
+ super.onAttachedToHierarchy(preferenceManager)
+
+ // Find out which font is currently active
+ selected = EmojiCompatFont.byId(
+ PreferenceManager.getDefaultSharedPreferences(context).getInt(key, 0)
+ )
+ // We'll use this later to determine if anything has changed
+ original = selected
+ summary = selected.getDisplay(context)
+ }
+
+ override fun onClick() {
+ val view = LayoutInflater.from(context).inflate(R.layout.dialog_emojicompat, null)
+ viewIds.forEachIndexed { index, viewId ->
+ setupItem(view.findViewById(viewId), FONTS[index])
+ }
+ AlertDialog.Builder(context)
+ .setView(view)
+ .setPositiveButton(android.R.string.ok) { _, _ -> onDialogOk() }
+ .setNegativeButton(android.R.string.cancel, null)
+ .show()
+ }
+
+ private fun setupItem(container: View, font: EmojiCompatFont) {
+ val title: TextView = container.findViewById(R.id.emojicompat_name)
+ val caption: TextView = container.findViewById(R.id.emojicompat_caption)
+ val thumb: ImageView = container.findViewById(R.id.emojicompat_thumb)
+ val download: ImageButton = container.findViewById(R.id.emojicompat_download)
+ val cancel: ImageButton = container.findViewById(R.id.emojicompat_download_cancel)
+ val radio: RadioButton = container.findViewById(R.id.emojicompat_radio)
+
+ // Initialize all the views
+ title.text = font.getDisplay(container.context)
+ caption.setText(font.caption)
+ thumb.setImageResource(font.img)
+
+ // There needs to be a list of all the radio buttons in order to uncheck them when one is selected
+ radioButtons.add(radio)
+ updateItem(font, container)
+
+ // Set actions
+ download.setOnClickListener { startDownload(font, container) }
+ cancel.setOnClickListener { cancelDownload(font, container) }
+ radio.setOnClickListener { radioButton: View -> select(font, radioButton as RadioButton) }
+ container.setOnClickListener { containerView: View ->
+ select(font, containerView.findViewById(R.id.emojicompat_radio))
+ }
+ }
+
+ private fun startDownload(font: EmojiCompatFont, container: View) {
+ val download: ImageButton = container.findViewById(R.id.emojicompat_download)
+ val caption: TextView = container.findViewById(R.id.emojicompat_caption)
+ val progressBar: ProgressBar = container.findViewById(R.id.emojicompat_progress)
+ val cancel: ImageButton = container.findViewById(R.id.emojicompat_download_cancel)
+
+ // Switch to downloading style
+ download.visibility = View.GONE
+ caption.visibility = View.INVISIBLE
+ progressBar.visibility = View.VISIBLE
+ progressBar.progress = 0
+ cancel.visibility = View.VISIBLE
+ font.downloadFontFile(context, okHttpClient)
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ { progress ->
+ // The progress is returned as a float between 0 and 1, or -1 if it could not determined
+ if (progress >= 0) {
+ progressBar.isIndeterminate = false
+ val max = progressBar.max.toFloat()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ progressBar.setProgress((max * progress).toInt(), true)
+ } else {
+ progressBar.progress = (max * progress).toInt()
+ }
+ } else {
+ progressBar.isIndeterminate = true
+ }
+ },
+ {
+ Toast.makeText(context, R.string.download_failed, Toast.LENGTH_SHORT).show()
+ updateItem(font, container)
+ },
+ {
+ finishDownload(font, container)
+ }
+ ).also { downloadDisposables[font.id] = it }
+
+
+ }
+
+ private fun cancelDownload(font: EmojiCompatFont, container: View) {
+ font.deleteDownloadedFile(container.context)
+ downloadDisposables[font.id]?.dispose()
+ downloadDisposables[font.id] = null
+ updateItem(font, container)
+ }
+
+ private fun finishDownload(font: EmojiCompatFont, container: View) {
+ select(font, container.findViewById(R.id.emojicompat_radio))
+ updateItem(font, container)
+ // Set the flag to restart the app (because an update has been downloaded)
+ if (selected === original && currentNeedsUpdate) {
+ updated = true
+ currentNeedsUpdate = false
+ }
+ }
+
+ /**
+ * Select a font both visually and logically
+ *
+ * @param font The font to be selected
+ * @param radio The radio button associated with it's visual item
+ */
+ private fun select(font: EmojiCompatFont, radio: RadioButton) {
+ selected = font
+ // Uncheck all the other buttons
+ for (other in radioButtons) {
+ if (other !== radio) {
+ other.isChecked = false
+ }
+ }
+ radio.isChecked = true
+ }
+
+ /**
+ * Called when a "consistent" state is reached, i.e. it's not downloading the font
+ *
+ * @param font The font to be displayed
+ * @param container The ConstraintLayout containing the item
+ */
+ private fun updateItem(font: EmojiCompatFont, container: View) {
+ // Assignments
+ val download: ImageButton = container.findViewById(R.id.emojicompat_download)
+ val caption: TextView = container.findViewById(R.id.emojicompat_caption)
+ val progress: ProgressBar = container.findViewById(R.id.emojicompat_progress)
+ val cancel: ImageButton = container.findViewById(R.id.emojicompat_download_cancel)
+ val radio: RadioButton = container.findViewById(R.id.emojicompat_radio)
+
+ // There's no download going on
+ progress.visibility = View.GONE
+ cancel.visibility = View.GONE
+ caption.visibility = View.VISIBLE
+ if (font.isDownloaded(context)) {
+ // Make it selectable
+ download.visibility = View.GONE
+ radio.visibility = View.VISIBLE
+ container.isClickable = true
+ } else {
+ // Make it downloadable
+ download.visibility = View.VISIBLE
+ radio.visibility = View.GONE
+ container.isClickable = false
+ }
+
+ // Select it if necessary
+ if (font === selected) {
+ radio.isChecked = true
+ // Update available
+ if (!font.isDownloaded(context)) {
+ currentNeedsUpdate = true
+ }
+ } else {
+ radio.isChecked = false
+ }
+ }
+
+ private fun saveSelectedFont() {
+ val index = selected.id
+ Log.i(TAG, "saveSelectedFont: Font ID: $index")
+ PreferenceManager
+ .getDefaultSharedPreferences(context)
+ .edit()
+ .putInt(key, index)
+ .apply()
+ summary = selected.getDisplay(context)
+ }
+
+ /**
+ * User clicked ok -> save the selected font and offer to restart the app if something changed
+ */
+ private fun onDialogOk() {
+ saveSelectedFont()
+ if (selected !== original || updated) {
+ AlertDialog.Builder(context)
+ .setTitle(R.string.restart_required)
+ .setMessage(R.string.restart_emoji)
+ .setNegativeButton(R.string.later, null)
+ .setPositiveButton(R.string.restart) { _, _ ->
+ // Restart the app
+ // From https://stackoverflow.com/a/17166729/5070653
+ val launchIntent = Intent(context, SplashActivity::class.java)
+ val mPendingIntent = PendingIntent.getActivity(
+ context,
+ 0x1f973, // This is the codepoint of the party face emoji :D
+ launchIntent,
+ PendingIntent.FLAG_CANCEL_CURRENT)
+ val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+ mgr.set(
+ AlarmManager.RTC,
+ System.currentTimeMillis() + 100,
+ mPendingIntent)
+ exitProcess(0)
+ }.show()
+ }
+ }
+
+ companion object {
+ private const val TAG = "EmojiPreference"
+
+ // Please note that this array must sorted in the same way as the fonts.
+ private val viewIds = intArrayOf(
+ R.id.item_nomoji,
+ R.id.item_blobmoji,
+ R.id.item_twemoji,
+ R.id.item_notoemoji
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/NotificationPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt
similarity index 99%
rename from app/src/main/java/com/keylesspalace/tusky/fragment/preference/NotificationPreferencesFragment.kt
rename to app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt
index 800ea7c1..f917d2bb 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/NotificationPreferencesFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt
@@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see . */
-package com.keylesspalace.tusky.fragment.preference
+package com.keylesspalace.tusky.components.preference
import android.os.Bundle
import androidx.preference.PreferenceFragmentCompat
diff --git a/app/src/main/java/com/keylesspalace/tusky/PreferencesActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt
similarity index 97%
rename from app/src/main/java/com/keylesspalace/tusky/PreferencesActivity.kt
rename to app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt
index 383a07ae..4fe0abd8 100644
--- a/app/src/main/java/com/keylesspalace/tusky/PreferencesActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt
@@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see . */
-package com.keylesspalace.tusky
+package com.keylesspalace.tusky.components.preference
import android.content.Context
import android.content.Intent
@@ -23,9 +23,11 @@ import android.util.Log
import android.view.MenuItem
import androidx.fragment.app.Fragment
import androidx.preference.PreferenceManager
+import com.keylesspalace.tusky.BaseActivity
+import com.keylesspalace.tusky.MainActivity
+import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
-import com.keylesspalace.tusky.fragment.preference.*
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.getNonNullString
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/PreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt
similarity index 96%
rename from app/src/main/java/com/keylesspalace/tusky/fragment/preference/PreferencesFragment.kt
rename to app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt
index 6dec7273..edddffca 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/PreferencesFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt
@@ -13,13 +13,13 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see . */
-package com.keylesspalace.tusky.fragment.preference
+package com.keylesspalace.tusky.components.preference
import android.os.Bundle
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
-import com.keylesspalace.tusky.PreferencesActivity
import com.keylesspalace.tusky.R
+import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.settings.*
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.getNonNullString
@@ -27,8 +27,13 @@ import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizePx
+import okhttp3.OkHttpClient
+import javax.inject.Inject
-class PreferencesFragment : PreferenceFragmentCompat() {
+class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
+
+ @Inject
+ lateinit var okhttpclient: OkHttpClient
private val iconSize by lazy { resources.getDimensionPixelSize(R.dimen.preference_icon_size) }
private var httpProxyPref: Preference? = null
@@ -47,7 +52,7 @@ class PreferencesFragment : PreferenceFragmentCompat() {
icon = makeIcon(GoogleMaterial.Icon.gmd_palette)
}
- emojiPreference {
+ emojiPreference(okhttpclient) {
setDefaultValue("system_default")
setIcon(R.drawable.ic_emoji_24dp)
key = PrefKeys.EMOJI
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/ProxyPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/ProxyPreferencesFragment.kt
similarity index 97%
rename from app/src/main/java/com/keylesspalace/tusky/fragment/preference/ProxyPreferencesFragment.kt
rename to app/src/main/java/com/keylesspalace/tusky/components/preference/ProxyPreferencesFragment.kt
index e7ee7ade..922d5a7a 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/ProxyPreferencesFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/ProxyPreferencesFragment.kt
@@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see . */
-package com.keylesspalace.tusky.fragment.preference
+package com.keylesspalace.tusky.components.preference
import android.os.Bundle
import androidx.preference.PreferenceFragmentCompat
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/TabFilterPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/TabFilterPreferencesFragment.kt
similarity index 97%
rename from app/src/main/java/com/keylesspalace/tusky/fragment/preference/TabFilterPreferencesFragment.kt
rename to app/src/main/java/com/keylesspalace/tusky/components/preference/TabFilterPreferencesFragment.kt
index cd76300d..71c5e10e 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/TabFilterPreferencesFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/TabFilterPreferencesFragment.kt
@@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see . */
-package com.keylesspalace.tusky.fragment.preference
+package com.keylesspalace.tusky.components.preference
import android.os.Bundle
import androidx.preference.PreferenceFragmentCompat
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt
index 5daf584b..3ecadd58 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt
@@ -20,7 +20,6 @@ import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
import androidx.activity.viewModels
-import androidx.lifecycle.Observer
import com.keylesspalace.tusky.BottomSheetActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.report.adapter.ReportPagerAdapter
@@ -77,7 +76,7 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector {
}
private fun subscribeObservables() {
- viewModel.navigation.observe(this, Observer { screen ->
+ viewModel.navigation.observe(this) { screen ->
if (screen != null) {
viewModel.navigated()
when (screen) {
@@ -88,14 +87,14 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector {
Screen.Finish -> closeScreen()
}
}
- })
+ }
- viewModel.checkUrl.observe(this, Observer {
+ viewModel.checkUrl.observe(this) {
if (!it.isNullOrBlank()) {
viewModel.urlChecked()
viewUrl(it)
}
- })
+ }
}
private fun showPreviousScreen() {
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt
index 8b1e5cda..fdc73163 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt
@@ -19,6 +19,9 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import androidx.paging.PagedList
+import com.keylesspalace.tusky.appstore.BlockEvent
+import com.keylesspalace.tusky.appstore.EventHub
+import com.keylesspalace.tusky.appstore.MuteEvent
import com.keylesspalace.tusky.components.report.adapter.StatusesRepository
import com.keylesspalace.tusky.components.report.model.StatusViewState
import com.keylesspalace.tusky.entity.Relationship
@@ -31,6 +34,7 @@ import javax.inject.Inject
class ReportViewModel @Inject constructor(
private val mastodonApi: MastodonApi,
+ private val eventHub: EventHub,
private val statusesRepository: StatusesRepository) : RxAwareViewModel() {
private val navigationMutable = MutableLiveData()
@@ -96,7 +100,7 @@ class ReportViewModel @Inject constructor(
val ids = listOf(accountId)
muteStateMutable.value = Loading()
blockStateMutable.value = Loading()
- mastodonApi.relationshipsObservable(ids)
+ mastodonApi.relationships(ids)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
@@ -123,16 +127,21 @@ class ReportViewModel @Inject constructor(
}
fun toggleMute() {
- if (muteStateMutable.value?.data == true) {
- mastodonApi.unmuteAccountObservable(accountId)
+ val alreadyMuted = muteStateMutable.value?.data == true
+ if (alreadyMuted) {
+ mastodonApi.unmuteAccount(accountId)
} else {
- mastodonApi.muteAccountObservable(accountId)
+ mastodonApi.muteAccount(accountId)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ relationship ->
- muteStateMutable.value = Success(relationship?.muting == true)
+ val muting = relationship?.muting == true
+ muteStateMutable.value = Success(muting)
+ if (muting) {
+ eventHub.dispatch(MuteEvent(accountId))
+ }
},
{ error ->
muteStateMutable.value = Error(false, error.message)
@@ -143,16 +152,21 @@ class ReportViewModel @Inject constructor(
}
fun toggleBlock() {
- if (blockStateMutable.value?.data == true) {
- mastodonApi.unblockAccountObservable(accountId)
+ val alreadyBlocked = blockStateMutable.value?.data == true
+ if (alreadyBlocked) {
+ mastodonApi.unblockAccount(accountId)
} else {
- mastodonApi.blockAccountObservable(accountId)
+ mastodonApi.blockAccount(accountId)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ relationship ->
- blockStateMutable.value = Success(relationship?.blocking == true)
+ val blocking = relationship?.blocking == true
+ blockStateMutable.value = Success(blocking)
+ if (blocking) {
+ eventHub.dispatch(BlockEvent(accountId))
+ }
},
{ error ->
blockStateMutable.value = Error(false, error.message)
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/AdapterHandler.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/AdapterHandler.kt
index 588cfef0..957d5b32 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/AdapterHandler.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/AdapterHandler.kt
@@ -16,10 +16,8 @@
package com.keylesspalace.tusky.components.report.adapter
import android.view.View
-import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.interfaces.LinkListener
-import java.util.ArrayList
interface AdapterHandler: LinkListener {
fun showMedia(v: View?, status: Status?, idx: Int)
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesRepository.kt
index 852a07f9..cea3080e 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesRepository.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesRepository.kt
@@ -21,7 +21,6 @@ import androidx.paging.toLiveData
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.BiListing
-import com.keylesspalace.tusky.util.Listing
import io.reactivex.disposables.CompositeDisposable
import java.util.concurrent.Executors
import javax.inject.Inject
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt
index 611231ec..6151903f 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt
@@ -22,7 +22,6 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
-import androidx.lifecycle.Observer
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.report.ReportViewModel
import com.keylesspalace.tusky.components.report.Screen
@@ -55,7 +54,7 @@ class ReportDoneFragment : Fragment(), Injectable {
}
private fun subscribeObservables() {
- viewModel.muteState.observe(viewLifecycleOwner, Observer {
+ viewModel.muteState.observe(viewLifecycleOwner) {
if (it !is Loading) {
buttonMute.show()
progressMute.show()
@@ -68,9 +67,9 @@ class ReportDoneFragment : Fragment(), Injectable {
true -> R.string.action_unmute
else -> R.string.action_mute
})
- })
+ }
- viewModel.blockState.observe(viewLifecycleOwner, Observer {
+ viewModel.blockState.observe(viewLifecycleOwner) {
if (it !is Loading) {
buttonBlock.show()
progressBlock.show()
@@ -83,7 +82,7 @@ class ReportDoneFragment : Fragment(), Injectable {
true -> R.string.action_unblock
else -> R.string.action_block
})
- })
+ }
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt
index 5a94ba17..4e7b00ab 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt
@@ -22,7 +22,6 @@ import android.view.ViewGroup
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
-import androidx.lifecycle.Observer
import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.report.ReportViewModel
@@ -81,14 +80,14 @@ class ReportNoteFragment : Fragment(), Injectable {
}
private fun subscribeObservables() {
- viewModel.reportingState.observe(viewLifecycleOwner, Observer {
+ viewModel.reportingState.observe(viewLifecycleOwner) {
when (it) {
is Success -> viewModel.navigateTo(Screen.Done)
is Loading -> showLoading()
is Error -> showError(it.cause)
}
- })
+ }
}
private fun showError(error: Throwable?) {
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt
index e5308efc..703acee2 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt
@@ -23,8 +23,6 @@ import androidx.core.app.ActivityOptionsCompat
import androidx.core.view.ViewCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
-import androidx.lifecycle.Observer
-import androidx.paging.PagedList
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
@@ -43,7 +41,10 @@ import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Status
-import com.keylesspalace.tusky.util.*
+import com.keylesspalace.tusky.util.CardViewMode
+import com.keylesspalace.tusky.util.StatusDisplayOptions
+import com.keylesspalace.tusky.util.hide
+import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.viewdata.AttachmentViewData
import kotlinx.android.synthetic.main.fragment_report_statuses.*
import javax.inject.Inject
@@ -129,11 +130,11 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler {
recyclerView.adapter = adapter
(recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
- viewModel.statuses.observe(viewLifecycleOwner, Observer> {
+ viewModel.statuses.observe(viewLifecycleOwner) {
adapter.submitList(it)
- })
+ }
- viewModel.networkStateAfter.observe(viewLifecycleOwner, Observer {
+ viewModel.networkStateAfter.observe(viewLifecycleOwner) {
if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING)
progressBarBottom.show()
else
@@ -141,9 +142,9 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler {
if (it?.status == com.keylesspalace.tusky.util.Status.FAILED)
showError(it.msg)
- })
+ }
- viewModel.networkStateBefore.observe(viewLifecycleOwner, Observer {
+ viewModel.networkStateBefore.observe(viewLifecycleOwner) {
if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING)
progressBarTop.show()
else
@@ -151,9 +152,9 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler {
if (it?.status == com.keylesspalace.tusky.util.Status.FAILED)
showError(it.msg)
- })
+ }
- viewModel.networkStateRefresh.observe(viewLifecycleOwner, Observer {
+ viewModel.networkStateRefresh.observe(viewLifecycleOwner) {
if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING && !swipeRefreshLayout.isRefreshing)
progressBarLoading.show()
else
@@ -163,7 +164,7 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler {
swipeRefreshLayout.isRefreshing = false
if (it?.status == com.keylesspalace.tusky.util.Status.FAILED)
showError(it.msg)
- })
+ }
}
private fun showError(@Suppress("UNUSED_PARAMETER") msg: String?) {
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt
index 2ade20b8..18b04df5 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt
@@ -19,7 +19,6 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
-import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
@@ -30,7 +29,6 @@ import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.ScheduledStatus
import com.keylesspalace.tusky.util.Status
-import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import kotlinx.android.synthetic.main.activity_scheduled_toot.*
@@ -68,11 +66,11 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec
viewModel = ViewModelProvider(this, viewModelFactory)[ScheduledTootViewModel::class.java]
- viewModel.data.observe(this, Observer {
+ viewModel.data.observe(this) {
adapter.submitList(it)
- })
+ }
- viewModel.networkState.observe(this, Observer { (status) ->
+ viewModel.networkState.observe(this) { (status) ->
when(status) {
Status.SUCCESS -> {
progressBar.hide()
@@ -103,9 +101,7 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec
}
}
}
-
- })
-
+ }
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@@ -124,6 +120,7 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec
override fun edit(item: ScheduledStatus) {
val intent = ComposeActivity.startIntent(this, ComposeActivity.ComposeOptions(
+ scheduledTootUid = item.id,
tootText = item.params.text,
contentWarning = item.params.spoilerText,
mediaAttachments = item.mediaAttachments,
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt
index c185bcc3..c5929c43 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt
@@ -7,7 +7,6 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData
-import androidx.lifecycle.Observer
import androidx.paging.PagedList
import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DividerItemDecoration
@@ -61,11 +60,11 @@ abstract class SearchFragment : Fragment(),
}
private fun subscribeObservables() {
- data.observe(viewLifecycleOwner, Observer {
+ data.observe(viewLifecycleOwner) {
adapter.submitList(it)
- })
+ }
- networkStateRefresh.observe(viewLifecycleOwner, Observer {
+ networkStateRefresh.observe(viewLifecycleOwner) {
searchProgressBar.visible(it == NetworkState.LOADING)
@@ -73,17 +72,16 @@ abstract class SearchFragment : Fragment(),
showError()
}
checkNoData()
+ }
- })
-
- networkState.observe(viewLifecycleOwner, Observer {
+ networkState.observe(viewLifecycleOwner) {
progressBarBottom.visible(it == NetworkState.LOADING)
if (it.status == Status.FAILED) {
showError()
}
- })
+ }
}
private fun checkNoData() {
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt
index 2a920881..ce5c2612 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt
@@ -26,8 +26,6 @@ import android.net.Uri
import android.os.Environment
import android.util.Log
import android.view.View
-import android.widget.CheckBox
-import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.PopupMenu
@@ -376,9 +374,10 @@ class SearchStatusesFragment : SearchFragment viewModel.muteAccount(accountId, notifications) }
- )
+ accountUsername
+ ) { notifications ->
+ viewModel.muteAccount(accountId, notifications)
+ }
}
private fun accountIsInMentions(account: AccountEntity?, mentions: Array): Boolean {
diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt
index 5293bd03..138f71a3 100644
--- a/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt
@@ -21,7 +21,6 @@ import com.keylesspalace.tusky.entity.Status
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
-import kotlin.Comparator
/**
* This class caches the account database and handles all account related operations
@@ -63,7 +62,7 @@ class AccountManager @Inject constructor(db: AppDatabase) {
accountDao.insertOrReplace(it)
}
- val maxAccountId = accounts.maxBy { it.id }?.id ?: 0
+ val maxAccountId = accounts.maxByOrNull { it.id }?.id ?: 0
val newAccountId = maxAccountId + 1
activeAccount = AccountEntity(id = newAccountId, domain = domain.toLowerCase(Locale.ROOT), accessToken = accessToken, isActive = true)
@@ -166,13 +165,13 @@ class AccountManager @Inject constructor(db: AppDatabase) {
*/
fun getAllAccountsOrderedByActive(): List {
val accountsCopy = accounts.toMutableList()
- accountsCopy.sortWith(Comparator { l, r ->
+ accountsCopy.sortWith { l, r ->
when {
l.isActive && !r.isActive -> -1
r.isActive && !l.isActive -> 1
else -> 0
}
- })
+ }
return accountsCopy
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt
index 35ca6fc9..9c50ad03 100644
--- a/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt
@@ -99,9 +99,8 @@ WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId =
AND serverId = :statusId""")
abstract fun delete(accountId: Long, statusId: String)
- @Query("""DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId
-AND authorServerId != :accountServerId AND createdAt < :olderThan""")
- abstract fun cleanup(accountId: Long, accountServerId: String, olderThan: Long)
+ @Query("""DELETE FROM TimelineStatusEntity WHERE createdAt < :olderThan""")
+ abstract fun cleanup(olderThan: Long)
@Query("""UPDATE TimelineStatusEntity SET poll = :poll
WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)""")
diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt
index 1f958e5a..0257c28f 100644
--- a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt
@@ -16,8 +16,10 @@
package com.keylesspalace.tusky.di
import com.keylesspalace.tusky.*
+import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
+import com.keylesspalace.tusky.components.preference.PreferencesActivity
import com.keylesspalace.tusky.components.report.ReportActivity
import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity
import com.keylesspalace.tusky.components.search.SearchActivity
@@ -102,4 +104,7 @@ abstract class ActivitiesModule {
@ContributesAndroidInjector
abstract fun contributesScheduledTootActivity(): ScheduledTootActivity
+
+ @ContributesAndroidInjector
+ abstract fun contributesAnnouncementsActivity(): AnnouncementsActivity
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt
index e87ef289..aedcc3ae 100644
--- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt
@@ -25,6 +25,8 @@ import androidx.room.Room
import com.keylesspalace.tusky.TuskyApplication
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.EventHubImpl
+import com.keylesspalace.tusky.components.notifications.Notifier
+import com.keylesspalace.tusky.components.notifications.SystemNotifier
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.network.TimelineCases
@@ -79,7 +81,11 @@ class AppModule {
AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19,
AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22,
AppDatabase.MIGRATION_22_23)
- .build()
+ .build()
}
+ @Provides
+ @Singleton
+ fun notifier(context: Context): Notifier = SystemNotifier(context)
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt
index 48e8b625..a1a8c8fe 100644
--- a/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt
@@ -20,14 +20,15 @@ import com.keylesspalace.tusky.AccountsInListFragment
import com.keylesspalace.tusky.components.conversation.ConversationsFragment
import com.keylesspalace.tusky.components.instancemute.fragment.InstanceListFragment
import com.keylesspalace.tusky.fragment.*
-import com.keylesspalace.tusky.fragment.preference.AccountPreferencesFragment
-import com.keylesspalace.tusky.fragment.preference.NotificationPreferencesFragment
+import com.keylesspalace.tusky.components.preference.AccountPreferencesFragment
+import com.keylesspalace.tusky.components.preference.NotificationPreferencesFragment
import com.keylesspalace.tusky.components.report.fragments.ReportDoneFragment
import com.keylesspalace.tusky.components.report.fragments.ReportNoteFragment
import com.keylesspalace.tusky.components.report.fragments.ReportStatusesFragment
import com.keylesspalace.tusky.components.search.fragments.SearchAccountsFragment
import com.keylesspalace.tusky.components.search.fragments.SearchHashtagsFragment
import com.keylesspalace.tusky.components.search.fragments.SearchStatusesFragment
+import com.keylesspalace.tusky.components.preference.PreferencesFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@@ -85,4 +86,7 @@ abstract class FragmentBuildersModule {
@ContributesAndroidInjector
abstract fun searchHashtagsFragment(): SearchHashtagsFragment
+ @ContributesAndroidInjector
+ abstract fun preferencesFragment(): PreferencesFragment
+
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt b/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt
index f4929463..c461012d 100644
--- a/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt
@@ -4,6 +4,7 @@ package com.keylesspalace.tusky.di
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
+import com.keylesspalace.tusky.components.announcements.AnnouncementsViewModel
import com.keylesspalace.tusky.components.compose.ComposeViewModel
import com.keylesspalace.tusky.components.conversation.ConversationsViewModel
import com.keylesspalace.tusky.components.report.ReportViewModel
@@ -85,5 +86,10 @@ abstract class ViewModelModule {
@ViewModelKey(ScheduledTootViewModel::class)
internal abstract fun scheduledTootViewModel(viewModel: ScheduledTootViewModel): ViewModel
+ @Binds
+ @IntoMap
+ @ViewModelKey(AnnouncementsViewModel::class)
+ internal abstract fun announcementsViewModel(viewModel: AnnouncementsViewModel): ViewModel
+
//Add more ViewModels here
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt
new file mode 100644
index 00000000..5cd32fe8
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt
@@ -0,0 +1,57 @@
+/* Copyright 2020 Tusky Contributors
+ *
+ * This file is a part of Tusky.
+ *
+ * This program is free software; you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tusky; if not,
+ * see . */
+
+package com.keylesspalace.tusky.entity
+
+import android.text.Spanned
+import com.google.gson.annotations.SerializedName
+import java.util.*
+
+data class Announcement(
+ val id: String,
+ val content: Spanned,
+ @SerializedName("starts_at") val startsAt: Date?,
+ @SerializedName("ends_at") val endsAt: Date?,
+ @SerializedName("all_day") val allDay: Boolean,
+ @SerializedName("published_at") val publishedAt: Date,
+ @SerializedName("updated_at") val updatedAt: Date,
+ val read: Boolean,
+ val mentions: List,
+ val statuses: List,
+ val tags: List,
+ val emojis: List,
+ val reactions: List
+) {
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || javaClass != other.javaClass) return false
+
+ val announcement = other as Announcement?
+ return id == announcement?.id
+ }
+
+ override fun hashCode(): Int {
+ return id.hashCode()
+ }
+
+ data class Reaction(
+ val name: String,
+ var count: Int,
+ var me: Boolean,
+ val url: String?,
+ @SerializedName("static_url") val staticUrl: String?
+ )
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt
index 43e54f01..ada9ec20 100644
--- a/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt
@@ -26,7 +26,9 @@ data class Card(
val image: String,
val type: String,
val width: Int,
- val height: Int
+ val height: Int,
+ val blurhash: String?,
+ val embed_url: String?
) {
override fun hashCode(): Int {
@@ -41,4 +43,7 @@ data class Card(
return account?.url == this.url
}
+ companion object {
+ const val TYPE_PHOTO = "photo"
+ }
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt
index baee54bc..fe7a22c7 100644
--- a/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt
@@ -23,5 +23,6 @@ import kotlinx.android.parcel.Parcelize
data class Emoji(
val shortcode: String,
val url: String,
+ @SerializedName("static_url") val staticUrl: String,
@SerializedName("visible_in_picker") val visibleInPicker: Boolean?
-) : Parcelable
\ No newline at end of file
+) : Parcelable
diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Marker.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Marker.kt
new file mode 100644
index 00000000..16fd9e31
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/entity/Marker.kt
@@ -0,0 +1,15 @@
+package com.keylesspalace.tusky.entity
+
+import com.google.gson.annotations.SerializedName
+import java.util.*
+
+/**
+ * API type for saving the scroll position of a timeline.
+ */
+data class Marker(
+ @SerializedName("last_read_id")
+ val lastReadId: String,
+ val version: Int,
+ @SerializedName("updated_at")
+ val updatedAt: Date
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.kt
index 0c974d19..e6a75c51 100644
--- a/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.kt
@@ -26,5 +26,6 @@ data class Relationship (
@SerializedName("muting_notifications") val mutingNotifications: Boolean,
val requested: Boolean,
@SerializedName("showing_reblogs") val showingReblogs: Boolean,
- @SerializedName("domain_blocking") val blockingDomain: Boolean
+ @SerializedName("domain_blocking") val blockingDomain: Boolean,
+ val note: String? // nullable for backward compatibility / feature detection
)
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt
index 9ec53b2d..d15835e3 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt
@@ -52,7 +52,6 @@ import java.io.IOException
import java.util.HashMap
import javax.inject.Inject
-
class AccountListFragment : BaseFragment(), AccountActionListener, Injectable {
@Inject
@@ -116,27 +115,17 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable {
}
override fun onMute(mute: Boolean, id: String, position: Int, notifications: Boolean) {
- val callback = object : Callback {
- override fun onResponse(call: Call, response: Response) {
- if (response.isSuccessful) {
- onMuteSuccess(mute, id, position, notifications)
- } else {
- onMuteFailure(mute, id, notifications)
- }
- }
-
- override fun onFailure(call: Call, t: Throwable) {
- onMuteFailure(mute, id, notifications)
- }
- }
-
- val call = if (!mute) {
+ if (!mute) {
api.unmuteAccount(id)
} else {
api.muteAccount(id, notifications)
}
- callList.add(call)
- call.enqueue(callback)
+ .autoDispose(from(this))
+ .subscribe({
+ onMuteSuccess(mute, id, position, notifications)
+ }, {
+ onMuteFailure(mute, id, notifications)
+ })
}
private fun onMuteSuccess(muted: Boolean, id: String, position: Int, notifications: Boolean) {
@@ -171,27 +160,17 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable {
}
override fun onBlock(block: Boolean, id: String, position: Int) {
- val cb = object : Callback {
- override fun onResponse(call: Call, response: Response) {
- if (response.isSuccessful) {
- onBlockSuccess(block, id, position)
- } else {
- onBlockFailure(block, id)
- }
- }
-
- override fun onFailure(call: Call, t: Throwable) {
- onBlockFailure(block, id)
- }
- }
-
- val call = if (!block) {
+ if (!block) {
api.unblockAccount(id)
} else {
api.blockAccount(id)
}
- callList.add(call)
- call.enqueue(cb)
+ .autoDispose(from(this))
+ .subscribe({
+ onBlockSuccess(block, id, position)
+ }, {
+ onBlockFailure(block, id)
+ })
}
private fun onBlockSuccess(blocked: Boolean, id: String, position: Int) {
@@ -350,29 +329,16 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable {
}
private fun fetchRelationships(ids: List) {
- val callback = object : Callback> {
- override fun onResponse(call: Call>, response: Response>) {
- val body = response.body()
- if (response.isSuccessful && body != null) {
- onFetchRelationshipsSuccess(body)
- } else {
+ api.relationships(ids)
+ .autoDispose(from(this))
+ .subscribe(::onFetchRelationshipsSuccess) {
onFetchRelationshipsFailure(ids)
}
- }
-
- override fun onFailure(call: Call>, t: Throwable) {
- onFetchRelationshipsFailure(ids)
- }
- }
-
- val call = api.relationships(ids)
- callList.add(call)
- call.enqueue(callback)
}
private fun onFetchRelationshipsSuccess(relationships: List) {
val mutesAdapter = adapter as MutesAdapter
- var mutingNotificationsMap = HashMap()
+ val mutingNotificationsMap = HashMap()
relationships.map { mutingNotificationsMap.put(it.id, it.mutingNotifications) }
mutesAdapter.updateMutingNotificationsMap(mutingNotificationsMap)
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java
index 3f84b3a6..8fe141c6 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java
+++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java
@@ -457,6 +457,7 @@ public abstract class SFragment extends BaseFragment implements Injectable {
composeOptions.setContentWarning(deletedStatus.getSpoilerText());
composeOptions.setMediaAttachments(deletedStatus.getAttachments());
composeOptions.setSensitive(deletedStatus.getSensitive());
+ composeOptions.setModifiedInitialState(true);
if (deletedStatus.getPoll() != null) {
composeOptions.setPoll(deletedStatus.getPoll().toNewPoll(deletedStatus.getCreatedAt()));
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java
index 71cc1181..ddcb0eb0 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java
+++ b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java
@@ -392,6 +392,7 @@ public class TimelineFragment extends SFragment implements
// home, notifications, public, thread
switch (kind) {
case HOME:
+ case LIST:
return filterContext.contains(Filter.HOME);
case PUBLIC_FEDERATED:
case PUBLIC_LOCAL:
@@ -1209,7 +1210,7 @@ public class TimelineFragment extends SFragment implements
if (status != null
&& ((status.getInReplyToId() != null && filterRemoveReplies)
|| (status.getReblog() != null && filterRemoveReblogs)
- || shouldFilterStatus(status))) {
+ || shouldFilterStatus(status.getActionableStatus()))) {
it.remove();
}
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt
index e61a24e5..fc4dfcb9 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt
@@ -99,9 +99,9 @@ class ViewImageFragment : ViewMediaFragment() {
url = attachment.url
description = attachment.description
} else {
- url = arguments.getString(ARG_AVATAR_URL)
+ url = arguments.getString(ARG_SINGLE_IMAGE_URL)
if (url == null) {
- throw IllegalArgumentException("attachment or avatar url has to be set")
+ throw IllegalArgumentException("attachment or image url has to be set")
}
}
@@ -158,6 +158,9 @@ class ViewImageFragment : ViewMediaFragment() {
}
private fun onGestureEnd() {
+ if (photoView == null) {
+ return
+ }
if (abs(photoView.translationY) > 180) {
photoActionsListener.onDismiss()
} else {
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt
index 9b51f285..86b3d09b 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt
@@ -42,7 +42,7 @@ abstract class ViewMediaFragment : BaseFragment() {
@JvmStatic
protected val ARG_ATTACHMENT = "attach"
@JvmStatic
- protected val ARG_AVATAR_URL = "avatarUrl"
+ protected val ARG_SINGLE_IMAGE_URL = "singleImageUrl"
@JvmStatic
fun newInstance(attachment: Attachment, shouldStartPostponedTransition: Boolean): ViewMediaFragment {
@@ -62,10 +62,10 @@ abstract class ViewMediaFragment : BaseFragment() {
}
@JvmStatic
- fun newAvatarInstance(avatarUrl: String): ViewMediaFragment {
+ fun newSingleImageInstance(imageUrl: String): ViewMediaFragment {
val arguments = Bundle(2)
val fragment = ViewImageFragment()
- arguments.putString(ARG_AVATAR_URL, avatarUrl)
+ arguments.putString(ARG_SINGLE_IMAGE_URL, imageUrl)
arguments.putBoolean(ARG_START_POSTPONED_TRANSITION, true)
fragment.arguments = arguments
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt
index 654dd40c..33d8f192 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt
@@ -189,7 +189,7 @@ class ViewVideoFragment : ViewMediaFragment() {
mediaDescription.animate().alpha(alpha)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
- mediaDescription.visible(isDescriptionVisible)
+ mediaDescription?.visible(isDescriptionVisible)
animation.removeListener(this)
}
})
diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt
index 767fe257..8f3dab3f 100644
--- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt
@@ -99,11 +99,19 @@ interface MastodonApi {
@Query("exclude_types[]") excludes: Set?
): Call>
+ @GET("api/v1/markers")
+ fun markersWithAuth(
+ @Header("Authorization") auth: String,
+ @Header(DOMAIN_HEADER) domain: String,
+ @Query("timeline[]") timelines: List
+ ): Single