diff options
author | Adolfo Santiago <epoch@nixnetmail.com> | 2023-02-25 09:04:55 +0100 |
---|---|---|
committer | Adolfo Santiago <epoch@nixnetmail.com> | 2023-02-25 09:04:55 +0100 |
commit | f6c18ed1eb91f0c4ca0095ad049a3095fb3d1f69 (patch) | |
tree | 2d9498c26c8eb58e63719e37b915a85610c5c877 | |
parent | 149022041b9f2f94b4f6052bd10d75d0a4b8c8a7 (diff) |
Update Java formattingtest_spotless
66 files changed, 6127 insertions, 5051 deletions
diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java b/husky/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java index 4b19039..36f670d 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java @@ -15,6 +15,8 @@ package com.keylesspalace.tusky; +import static org.koin.java.KoinJavaComponent.inject; + import android.app.ActivityManager; import android.content.Context; import android.content.Intent; @@ -44,7 +46,6 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import kotlin.Lazy; -import static org.koin.java.KoinJavaComponent.inject; public abstract class BaseActivity extends AppCompatActivity { @@ -64,7 +65,7 @@ public abstract class BaseActivity extends AppCompatActivity { * views are created. */ String theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT); Log.d("activeTheme", theme); - if(theme.equals("black")) { + if (theme.equals("black")) { setTheme(R.style.TuskyBlackTheme); } @@ -74,12 +75,12 @@ public abstract class BaseActivity extends AppCompatActivity { int recentsBackgroundColor = ThemeUtils.getColor(this, R.attr.colorSurface); setTaskDescription( - new ActivityManager.TaskDescription(appName, appIcon, recentsBackgroundColor)); + new ActivityManager.TaskDescription(appName, appIcon, recentsBackgroundColor)); int style = textStyle(preferences.getString("statusTextSize", "medium")); getTheme().applyStyle(style, false); - if(requiresLogin()) { + if (requiresLogin()) { redirectIfNotLoggedIn(); } @@ -97,7 +98,7 @@ public abstract class BaseActivity extends AppCompatActivity { private static int textStyle(String name) { int style; - switch(name) { + switch (name) { case "smallest": style = R.style.TextSizeSmallest; break; @@ -135,7 +136,7 @@ public abstract class BaseActivity extends AppCompatActivity { protected void redirectIfNotLoggedIn() { AccountEntity account = accountManager.getValue().getActiveAccount(); - if(account == null) { + if (account == null) { Intent intent = new Intent(this, LoginActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); startActivityWithSlideInAnimation(intent); @@ -143,33 +144,33 @@ public abstract class BaseActivity extends AppCompatActivity { } } - protected void showErrorDialog(View anyView, - @StringRes int descriptionId, - @StringRes int actionId, - View.OnClickListener listener) - { - if(anyView != null) { + protected void showErrorDialog( + View anyView, + @StringRes int descriptionId, + @StringRes int actionId, + View.OnClickListener listener) { + if (anyView != null) { Snackbar bar = Snackbar.make(anyView, getString(descriptionId), Snackbar.LENGTH_SHORT); bar.setAction(actionId, listener); bar.show(); } } - public void showAccountChooserDialog(CharSequence dialogTitle, - boolean showActiveAccount, - AccountSelectionListener listener) - { + public void showAccountChooserDialog( + CharSequence dialogTitle, + boolean showActiveAccount, + AccountSelectionListener listener) { List<AccountEntity> accounts = accountManager.getValue().getAllAccountsOrderedByActive(); AccountEntity activeAccount = accountManager.getValue().getActiveAccount(); - switch(accounts.size()) { + switch (accounts.size()) { case 1: listener.onAccountSelected(activeAccount); return; case 2: - if(!showActiveAccount) { - for(AccountEntity account : accounts) { - if(activeAccount != account) { + if (!showActiveAccount) { + for (AccountEntity account : accounts) { + if (activeAccount != account) { listener.onAccountSelected(account); return; } @@ -178,22 +179,24 @@ public abstract class BaseActivity extends AppCompatActivity { break; } - if(!showActiveAccount && activeAccount != null) { + if (!showActiveAccount && activeAccount != null) { accounts.remove(activeAccount); } AccountSelectionAdapter adapter = new AccountSelectionAdapter(this); adapter.addAll(accounts); - new AlertDialog.Builder(this).setTitle(dialogTitle).setAdapter(adapter, - (dialogInterface, index) -> listener.onAccountSelected(accounts.get(index))).show(); + new AlertDialog.Builder(this) + .setTitle(dialogTitle) + .setAdapter( + adapter, + (dialogInterface, index) -> listener.onAccountSelected(accounts.get(index))) + .show(); } @Override - public void onRequestPermissionsResult(int requestCode, - @NonNull String[] permissions, - @NonNull int[] grantResults) - { - if(requesters.containsKey(requestCode)) { + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (requesters.containsKey(requestCode)) { PermissionRequester requester = requesters.remove(requestCode); requester.onRequestPermissionsResult(permissions, grantResults); } @@ -201,15 +204,15 @@ public abstract class BaseActivity extends AppCompatActivity { public void requestPermissions(String[] permissions, PermissionRequester requester) { ArrayList<String> permissionsToRequest = new ArrayList<>(); - for(String permission : permissions) { - if(ContextCompat.checkSelfPermission(this, permission) != - PackageManager.PERMISSION_GRANTED) { + for (String permission : permissions) { + if (ContextCompat.checkSelfPermission(this, permission) + != PackageManager.PERMISSION_GRANTED) { permissionsToRequest.add(permission); } } - if(permissionsToRequest.isEmpty()) { + if (permissionsToRequest.isEmpty()) { int[] permissionsAlreadyGranted = new int[permissions.length]; - for(int i = 0; i < permissionsAlreadyGranted.length; ++i) { + for (int i = 0; i < permissionsAlreadyGranted.length; ++i) { permissionsAlreadyGranted[i] = PackageManager.PERMISSION_GRANTED; } requester.onRequestPermissionsResult(permissions, permissionsAlreadyGranted); @@ -217,12 +220,11 @@ public abstract class BaseActivity extends AppCompatActivity { } int newKey = requester == null ? REQUESTER_NONE : requesters.size(); - if(newKey != REQUESTER_NONE) { + if (newKey != REQUESTER_NONE) { requesters.put(newKey, requester); } String[] permissionsCopy = new String[permissionsToRequest.size()]; permissionsToRequest.toArray(permissionsCopy); ActivityCompat.requestPermissions(this, permissionsCopy, newKey); - } } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java b/husky/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java index a459af4..cc19d49 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java @@ -15,6 +15,11 @@ package com.keylesspalace.tusky; +import static com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions; +import static com.uber.autodispose.AutoDispose.autoDisposable; +import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from; +import static org.koin.java.KoinJavaComponent.inject; + import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; @@ -33,50 +38,45 @@ import com.keylesspalace.tusky.adapter.SavedTootAdapter; import com.keylesspalace.tusky.appstore.EventHub; import com.keylesspalace.tusky.appstore.StatusComposedEvent; import com.keylesspalace.tusky.components.compose.ComposeActivity; -import static com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions; import com.keylesspalace.tusky.db.AppDatabase; import com.keylesspalace.tusky.db.TootDao; import com.keylesspalace.tusky.db.TootEntity; import com.keylesspalace.tusky.util.SaveTootHelper; import com.keylesspalace.tusky.view.BackgroundMessageView; -import static com.uber.autodispose.AutoDispose.autoDisposable; -import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from; import io.reactivex.android.schedulers.AndroidSchedulers; import java.lang.ref.WeakReference; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; -import static org.koin.java.KoinJavaComponent.inject; public final class SavedTootActivity extends BaseActivity - implements SavedTootAdapter.SavedTootAction -{ + implements SavedTootAdapter.SavedTootAction { // ui private SavedTootAdapter adapter; private BackgroundMessageView errorMessageView; private List<TootEntity> toots = new ArrayList<>(); - @Nullable - private AsyncTask<?, ?, ?> asyncTask; + @Nullable private AsyncTask<?, ?, ?> asyncTask; private final EventHub eventHub = (EventHub) inject(EventHub.class).getValue(); private final AppDatabase database = (AppDatabase) inject(AppDatabase.class).getValue(); private final SaveTootHelper saveTootHelper = - (SaveTootHelper) inject(SaveTootHelper.class).getValue(); + (SaveTootHelper) inject(SaveTootHelper.class).getValue(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - eventHub.getEvents().observeOn(AndroidSchedulers.mainThread()) - .ofType(StatusComposedEvent.class) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) - .subscribe((__) -> this.fetchToots()); + eventHub.getEvents() + .observeOn(AndroidSchedulers.mainThread()) + .ofType(StatusComposedEvent.class) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe((__) -> this.fetchToots()); setContentView(R.layout.activity_saved_toot); Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar bar = getSupportActionBar(); - if(bar != null) { + if (bar != null) { bar.setTitle(getString(R.string.title_drafts)); bar.setDisplayHomeAsUpEnabled(true); bar.setDisplayShowHomeEnabled(true); @@ -88,7 +88,7 @@ public final class SavedTootActivity extends BaseActivity LinearLayoutManager layoutManager = new LinearLayoutManager(this); recyclerView.setLayoutManager(layoutManager); DividerItemDecoration divider = - new DividerItemDecoration(this, layoutManager.getOrientation()); + new DividerItemDecoration(this, layoutManager.getOrientation()); recyclerView.addItemDecoration(divider); adapter = new SavedTootAdapter(this); recyclerView.setAdapter(adapter); @@ -103,31 +103,33 @@ public final class SavedTootActivity extends BaseActivity @Override protected void onPause() { super.onPause(); - if(asyncTask != null) { + if (asyncTask != null) { asyncTask.cancel(true); } } @Override public boolean onOptionsItemSelected(MenuItem item) { - switch(item.getItemId()) { - case android.R.id.home: { - onBackPressed(); - return true; - } + switch (item.getItemId()) { + case android.R.id.home: + { + onBackPressed(); + return true; + } } return super.onOptionsItemSelected(item); } private void fetchToots() { - asyncTask = new FetchPojosTask(this, database.tootDao()).executeOnExecutor( - AsyncTask.THREAD_POOL_EXECUTOR); + asyncTask = + new FetchPojosTask(this, database.tootDao()) + .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } private void setNoContent(int size) { - if(size == 0) { - errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_saved_status, - null); + if (size == 0) { + errorMessageView.setup( + R.drawable.elephant_friend_empty, R.string.no_saved_status, null); errorMessageView.setVisibility(View.VISIBLE); } else { errorMessageView.setVisibility(View.GONE); @@ -141,7 +143,7 @@ public final class SavedTootActivity extends BaseActivity toots.remove(position); // update adapter - if(adapter != null) { + if (adapter != null) { adapter.removeItem(position); setNoContent(toots.size()); } @@ -150,23 +152,32 @@ public final class SavedTootActivity extends BaseActivity @Override public void click(int position, TootEntity item) { Gson gson = new Gson(); - Type stringListType = new TypeToken<List<String>>() { - }.getType(); + Type stringListType = new TypeToken<List<String>>() {}.getType(); List<String> jsonUrls = gson.fromJson(item.getUrls(), stringListType); List<String> descriptions = gson.fromJson(item.getDescriptions(), stringListType); - ComposeOptions composeOptions = new ComposeOptions( - /*scheduledTootUid*/null, item.getUid(), - /*drafId*/null, item.getText(), jsonUrls, descriptions, - /*mentionedUsernames*/null, item.getInReplyToId(), - /*replyVisibility*/null, item.getVisibility(), item.getContentWarning(), - item.getInReplyToUsername(), item.getInReplyToText(), - /*mediaAttachments*/null, - /*draftAttachments*/null, - /*scheduledAt*/null, - /*sensitive*/null, - /*poll*/null, item.getFormattingSyntax(), - /* modifiedInitialState */ true); + ComposeOptions composeOptions = + new ComposeOptions( + /*scheduledTootUid*/ null, + item.getUid(), + /*drafId*/ null, + item.getText(), + jsonUrls, + descriptions, + /*mentionedUsernames*/ null, + item.getInReplyToId(), + /*replyVisibility*/ null, + item.getVisibility(), + item.getContentWarning(), + item.getInReplyToUsername(), + item.getInReplyToText(), + /*mediaAttachments*/ null, + /*draftAttachments*/ null, + /*scheduledAt*/ null, + /*sensitive*/ null, + /*poll*/ null, + item.getFormattingSyntax(), + /* modifiedInitialState */ true); Intent intent = ComposeActivity.startIntent(this, composeOptions); startActivity(intent); } @@ -190,7 +201,7 @@ public final class SavedTootActivity extends BaseActivity protected void onPostExecute(List<TootEntity> pojos) { super.onPostExecute(pojos); SavedTootActivity activity = activityRef.get(); - if(activity == null) { + if (activity == null) { return; } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java b/husky/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java index 9d18494..3ca6fba 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java @@ -48,7 +48,7 @@ public class ViewTagActivity extends BottomSheetActivity { setSupportActionBar(toolbar); ActionBar bar = getSupportActionBar(); - if(bar != null) { + if (bar != null) { bar.setTitle(String.format(getString(R.string.title_tag), hashtag)); bar.setDisplayHomeAsUpEnabled(true); bar.setDisplayShowHomeEnabled(true); @@ -62,11 +62,12 @@ public class ViewTagActivity extends BottomSheetActivity { @Override public boolean onOptionsItemSelected(MenuItem item) { - switch(item.getItemId()) { - case android.R.id.home: { - onBackPressed(); - return true; - } + switch (item.getItemId()) { + case android.R.id.home: + { + onBackPressed(); + return true; + } } return super.onOptionsItemSelected(item); } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java b/husky/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java index 74a8f79..39e4048 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java @@ -55,7 +55,7 @@ public class ViewThreadActivity extends BottomSheetActivity { Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar actionBar = getSupportActionBar(); - if(actionBar != null) { + if (actionBar != null) { actionBar.setTitle(R.string.title_view_thread); actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setDisplayShowHomeEnabled(true); @@ -64,8 +64,9 @@ public class ViewThreadActivity extends BottomSheetActivity { String id = getIntent().getStringExtra(ID_EXTRA); fragment = - (ViewThreadFragment) getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG + id); - if(fragment == null) { + (ViewThreadFragment) + getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG + id); + if (fragment == null) { fragment = ViewThreadFragment.newInstance(id); } @@ -79,13 +80,15 @@ public class ViewThreadActivity extends BottomSheetActivity { getMenuInflater().inflate(R.menu.view_thread_toolbar, menu); MenuItem menuItem = menu.findItem(R.id.action_reveal); menuItem.setVisible(revealButtonState != REVEAL_BUTTON_HIDDEN); - menuItem.setIcon(revealButtonState == REVEAL_BUTTON_REVEAL ? R.drawable.ic_eye_24dp : - R.drawable.ic_hide_media_24dp); + menuItem.setIcon( + revealButtonState == REVEAL_BUTTON_REVEAL + ? R.drawable.ic_eye_24dp + : R.drawable.ic_hide_media_24dp); return super.onCreateOptionsMenu(menu); } public void setRevealButtonState(int state) { - switch(state) { + switch (state) { case REVEAL_BUTTON_HIDDEN: case REVEAL_BUTTON_REVEAL: case REVEAL_BUTTON_HIDE: @@ -99,15 +102,17 @@ public class ViewThreadActivity extends BottomSheetActivity { @Override public boolean onOptionsItemSelected(MenuItem item) { - switch(item.getItemId()) { - case android.R.id.home: { - onBackPressed(); - return true; - } - case R.id.action_reveal: { - fragment.onRevealPressed(); - return true; - } + switch (item.getItemId()) { + case android.R.id.home: + { + onBackPressed(); + return true; + } + case R.id.action_reveal: + { + fragment.onRevealPressed(); + return true; + } } return super.onOptionsItemSelected(item); } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java index 5c52e39..778fc05 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java @@ -18,11 +18,9 @@ package com.keylesspalace.tusky.adapter; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; - import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.interfaces.AccountActionListener; import com.keylesspalace.tusky.util.ListUtils; - import java.util.ArrayList; import java.util.List; @@ -70,11 +68,11 @@ public abstract class AccountAdapter extends RecyclerView.Adapter { public void setBottomLoading(boolean loading) { boolean wasLoading = bottomLoading; - if(wasLoading == loading) { + if (wasLoading == loading) { return; } bottomLoading = loading; - if(loading) { + if (loading) { notifyItemInserted(accountList.size()); } else { notifyItemRemoved(accountList.size()); @@ -107,6 +105,4 @@ public abstract class AccountAdapter extends RecyclerView.Adapter { accountList.add(position, account); notifyItemInserted(position); } - - } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java index 3ac5968..1b8672e 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java @@ -4,10 +4,8 @@ import android.content.SharedPreferences; import android.view.View; import android.widget.ImageView; import android.widget.TextView; - 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; @@ -30,7 +28,8 @@ public class AccountViewHolder extends RecyclerView.ViewHolder { displayName = itemView.findViewById(R.id.account_display_name); avatar = itemView.findViewById(R.id.account_avatar); avatarInset = itemView.findViewById(R.id.account_avatar_inset); - SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(itemView.getContext()); + SharedPreferences sharedPrefs = + PreferenceManager.getDefaultSharedPreferences(itemView.getContext()); showBotOverlay = sharedPrefs.getBoolean("showBotOverlay", true); animateAvatar = sharedPrefs.getBoolean("animateGifAvatars", false); } @@ -40,10 +39,14 @@ public class AccountViewHolder extends RecyclerView.ViewHolder { String format = username.getContext().getString(R.string.status_username_format); String formattedUsername = String.format(format, account.getUsername()); username.setText(formattedUsername); - CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName, true); + CharSequence emojifiedName = + CustomEmojiHelper.emojify( + account.getName(), account.getEmojis(), displayName, true); displayName.setText(emojifiedName); - int avatarRadius = avatar.getContext().getResources() - .getDimensionPixelSize(R.dimen.avatar_radius_48dp); + int avatarRadius = + avatar.getContext() + .getResources() + .getDimensionPixelSize(R.dimen.avatar_radius_48dp); ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius, animateAvatar); if (showBotOverlay && account.getBot()) { avatarInset.setVisibility(View.VISIBLE); @@ -61,4 +64,4 @@ public class AccountViewHolder extends RecyclerView.ViewHolder { public void setupLinkListener(final LinkListener listener) { itemView.setOnClickListener(v -> listener.onViewAccount(accountId)); } -}
\ No newline at end of file +} diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java index 073d76d..77ca437 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java @@ -21,11 +21,9 @@ import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; - import androidx.annotation.NonNull; 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; @@ -43,16 +41,20 @@ public class BlocksAdapter extends AccountAdapter { public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { switch (viewType) { default: - case VIEW_TYPE_ACCOUNT: { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_blocked_user, parent, false); - return new BlockedUserViewHolder(view); - } - case VIEW_TYPE_FOOTER: { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_footer, parent, false); - return new LoadingFooterViewHolder(view); - } + case VIEW_TYPE_ACCOUNT: + { + View view = + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_blocked_user, parent, false); + return new BlockedUserViewHolder(view); + } + case VIEW_TYPE_FOOTER: + { + View view = + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_footer, parent, false); + return new LoadingFooterViewHolder(view); + } } } @@ -79,30 +81,34 @@ public class BlocksAdapter extends AccountAdapter { username = itemView.findViewById(R.id.blocked_user_username); displayName = itemView.findViewById(R.id.blocked_user_display_name); unblock = itemView.findViewById(R.id.blocked_user_unblock); - animateAvatar = PreferenceManager.getDefaultSharedPreferences(itemView.getContext()) - .getBoolean("animateGifAvatars", false); - + animateAvatar = + PreferenceManager.getDefaultSharedPreferences(itemView.getContext()) + .getBoolean("animateGifAvatars", false); } void setupWithAccount(Account account) { id = account.getId(); - CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName); + CharSequence emojifiedName = + CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName); displayName.setText(emojifiedName); String format = username.getContext().getString(R.string.status_username_format); String formattedUsername = String.format(format, account.getUsername()); username.setText(formattedUsername); - int avatarRadius = avatar.getContext().getResources() - .getDimensionPixelSize(R.dimen.avatar_radius_48dp); + int avatarRadius = + avatar.getContext() + .getResources() + .getDimensionPixelSize(R.dimen.avatar_radius_48dp); ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius, animateAvatar); } void setupActionListener(final AccountActionListener listener) { - unblock.setOnClickListener(v -> { - int position = getAdapterPosition(); - if (position != RecyclerView.NO_POSITION) { - listener.onBlock(false, id, position); - } - }); + unblock.setOnClickListener( + v -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onBlock(false, id, position); + } + }); itemView.setOnClickListener(v -> listener.onViewAccount(id)); } } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiReactionsAdapter.java b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiReactionsAdapter.java index 658c28d..ef22f8a 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiReactionsAdapter.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiReactionsAdapter.java @@ -39,7 +39,10 @@ public class EmojiReactionsAdapter extends RecyclerView.Adapter<SingleViewHolder private final StatusActionListener listener; private final String statusId; - EmojiReactionsAdapter(final List<EmojiReaction> reactions, final StatusActionListener listener, final String statusId) { + EmojiReactionsAdapter( + final List<EmojiReaction> reactions, + final StatusActionListener listener, + final String statusId) { this.reactions = reactions; this.listener = listener; this.statusId = statusId; @@ -47,32 +50,33 @@ public class EmojiReactionsAdapter extends RecyclerView.Adapter<SingleViewHolder @Override public SingleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_emoji_reaction, parent, false); + View view = + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_emoji_reaction, parent, false); return new SingleViewHolder(view); } @Override public void onBindViewHolder(SingleViewHolder holder, int position) { EmojiReaction reaction = reactions.get(position); - SpannableStringBuilder builder = new SpannableStringBuilder( - reaction.getName() + " " + reaction.getCount()); + SpannableStringBuilder builder = + new SpannableStringBuilder(reaction.getName() + " " + reaction.getCount()); EmojiAppCompatButton btn = (EmojiAppCompatButton) holder.itemView; var url = reaction.getUrl(); - if(url != null) { + if (url != null) { var span = CustomEmojiHelper.createEmojiSpan(url, btn, true); - builder.setSpan(span, 0, reaction.getName().length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + builder.setSpan(span, 0, reaction.getName().length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } btn.setText(builder, BufferType.SPANNABLE); btn.setActivated(reaction.getMe()); - btn.setOnClickListener(v -> { - listener.onEmojiReactMenu(v, reaction, statusId); - }); + btn.setOnClickListener( + v -> { + listener.onEmojiReactMenu(v, reaction, statusId); + }); } // total number of rows @@ -81,4 +85,3 @@ public class EmojiReactionsAdapter extends RecyclerView.Adapter<SingleViewHolder return reactions.size(); } } - diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java index 8215874..3ecc921 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java @@ -15,12 +15,11 @@ package com.keylesspalace.tusky.adapter; -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.interfaces.AccountActionListener; @@ -36,16 +35,20 @@ public class FollowAdapter extends AccountAdapter { public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { switch (viewType) { default: - case VIEW_TYPE_ACCOUNT: { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_account, parent, false); - return new AccountViewHolder(view); - } - case VIEW_TYPE_FOOTER: { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_footer, parent, false); - return new LoadingFooterViewHolder(view); - } + case VIEW_TYPE_ACCOUNT: + { + View view = + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_account, parent, false); + return new AccountViewHolder(view); + } + case VIEW_TYPE_FOOTER: + { + View view = + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_footer, parent, false); + return new LoadingFooterViewHolder(view); + } } } @@ -57,5 +60,4 @@ public class FollowAdapter extends AccountAdapter { holder.setupActionListener(accountActionListener); } } - } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java index 307823b..788cae3 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java @@ -18,10 +18,8 @@ package com.keylesspalace.tusky.adapter; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; - import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding; import com.keylesspalace.tusky.interfaces.AccountActionListener; @@ -37,17 +35,20 @@ public class FollowRequestsAdapter extends AccountAdapter { public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { switch (viewType) { default: - case VIEW_TYPE_ACCOUNT: { - ItemFollowRequestBinding binding = ItemFollowRequestBinding.inflate( - LayoutInflater.from(parent.getContext()), parent, false - ); - return new FollowRequestViewHolder(binding, false); - } - case VIEW_TYPE_FOOTER: { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_footer, parent, false); - return new LoadingFooterViewHolder(view); - } + case VIEW_TYPE_ACCOUNT: + { + ItemFollowRequestBinding binding = + ItemFollowRequestBinding.inflate( + LayoutInflater.from(parent.getContext()), parent, false); + return new FollowRequestViewHolder(binding, false); + } + case VIEW_TYPE_FOOTER: + { + View view = + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_footer, parent, false); + return new LoadingFooterViewHolder(view); + } } } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/MutedStatusViewHolder.java b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/MutedStatusViewHolder.java index 66138b2..36374ee 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/MutedStatusViewHolder.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/MutedStatusViewHolder.java @@ -5,11 +5,9 @@ import android.text.format.DateUtils; import android.view.View; import android.widget.ImageButton; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; - import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.entity.Emoji; import com.keylesspalace.tusky.interfaces.StatusActionListener; @@ -17,7 +15,6 @@ import com.keylesspalace.tusky.util.CustomEmojiHelper; import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.util.TimestampUtils; import com.keylesspalace.tusky.viewdata.StatusViewData; - import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; @@ -42,13 +39,14 @@ public class MutedStatusViewHolder extends RecyclerView.ViewHolder { username = itemView.findViewById(R.id.status_username); timestampInfo = itemView.findViewById(R.id.status_timestamp_info); unmuteButton = itemView.findViewById(R.id.status_toggle_mute); - + this.shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()); this.longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault()); } protected void setDisplayName(String name, List<Emoji> customEmojis) { - CharSequence emojifiedName = CustomEmojiHelper.emojify(name, customEmojis, displayName, true); + CharSequence emojifiedName = + CustomEmojiHelper.emojify(name, customEmojis, displayName, true); displayName.setText(emojifiedName); } @@ -67,7 +65,9 @@ public class MutedStatusViewHolder extends RecyclerView.ViewHolder { } else { long then = createdAt.getTime(); long now = System.currentTimeMillis(); - String readout = TimestampUtils.getRelativeTimeSpanString(timestampInfo.getContext(), then, now); + String readout = + TimestampUtils.getRelativeTimeSpanString( + timestampInfo.getContext(), then, now); timestampInfo.setText(readout); } } @@ -84,8 +84,8 @@ public class MutedStatusViewHolder extends RecyclerView.ViewHolder { } } - private CharSequence getCreatedAtDescription(Date createdAt, - StatusDisplayOptions statusDisplayOptions) { + private CharSequence getCreatedAtDescription( + Date createdAt, StatusDisplayOptions statusDisplayOptions) { if (statusDisplayOptions.useAbsoluteTime()) { return getAbsoluteTime(createdAt); } else { @@ -97,57 +97,61 @@ public class MutedStatusViewHolder extends RecyclerView.ViewHolder { } else { long then = createdAt.getTime(); long now = System.currentTimeMillis(); - return DateUtils.getRelativeTimeSpanString(then, now, - DateUtils.SECOND_IN_MILLIS, - DateUtils.FORMAT_ABBREV_RELATIVE); + return DateUtils.getRelativeTimeSpanString( + then, now, DateUtils.SECOND_IN_MILLIS, DateUtils.FORMAT_ABBREV_RELATIVE); } } } - private void setDescriptionForStatus(@NonNull StatusViewData.Concrete status, - StatusDisplayOptions statusDisplayOptions) { + private void setDescriptionForStatus( + @NonNull StatusViewData.Concrete status, StatusDisplayOptions statusDisplayOptions) { Context context = itemView.getContext(); - String description = context.getString(R.string.description_muted_status, - status.getUserFullName(), - getCreatedAtDescription(status.getCreatedAt(), statusDisplayOptions), - status.getNickname() - ); + String description = + context.getString( + R.string.description_muted_status, + status.getUserFullName(), + getCreatedAtDescription(status.getCreatedAt(), statusDisplayOptions), + status.getNickname()); itemView.setContentDescription(description); } - protected void setupButtons(final StatusActionListener listener, final String accountId) { - - unmuteButton.setOnClickListener(v -> { - int position = getAdapterPosition(); - if (position != RecyclerView.NO_POSITION) { - listener.onMute(position, false); - } - }); - - itemView.setOnClickListener( v -> { - int position = getAdapterPosition(); - if (position != RecyclerView.NO_POSITION) { - listener.onViewThread(position); - } - }); + + unmuteButton.setOnClickListener( + v -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onMute(position, false); + } + }); + + itemView.setOnClickListener( + v -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onViewThread(position); + } + }); } - public void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener, - StatusDisplayOptions statusDisplayOptions) { + public void setupWithStatus( + StatusViewData.Concrete status, + final StatusActionListener listener, + StatusDisplayOptions statusDisplayOptions) { this.setupWithStatus(status, listener, statusDisplayOptions, null); } - protected void setupWithStatus(StatusViewData.Concrete status, - final StatusActionListener listener, - StatusDisplayOptions statusDisplayOptions, - @Nullable Object payloads) { + protected void setupWithStatus( + StatusViewData.Concrete status, + final StatusActionListener listener, + StatusDisplayOptions statusDisplayOptions, + @Nullable Object payloads) { if (payloads == null) { setDisplayName(status.getUserFullName(), status.getAccountEmojis()); setUsername(status.getNickname()); setCreatedAt(status.getCreatedAt(), statusDisplayOptions); - + setupButtons(listener, status.getSenderId()); setDescriptionForStatus(status, statusDisplayOptions); @@ -164,7 +168,6 @@ public class MutedStatusViewHolder extends RecyclerView.ViewHolder { setCreatedAt(status.getCreatedAt(), statusDisplayOptions); } } - } } } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java index c4224c9..3f67c37 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java @@ -6,18 +6,15 @@ import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.core.view.ViewCompat; 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.CustomEmojiHelper; import com.keylesspalace.tusky.util.ImageLoadingHelper; - import java.util.HashMap; public class MutesAdapter extends AccountAdapter { @@ -33,16 +30,20 @@ public class MutesAdapter extends AccountAdapter { public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { switch (viewType) { default: - case VIEW_TYPE_ACCOUNT: { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_muted_user, parent, false); - return new MutesAdapter.MutedUserViewHolder(view); - } - case VIEW_TYPE_FOOTER: { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_footer, parent, false); - return new LoadingFooterViewHolder(view); - } + case VIEW_TYPE_ACCOUNT: + { + View view = + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_muted_user, parent, false); + return new MutesAdapter.MutedUserViewHolder(view); + } + case VIEW_TYPE_FOOTER: + { + View view = + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_footer, parent, false); + return new LoadingFooterViewHolder(view); + } } } @@ -83,22 +84,27 @@ public class MutesAdapter extends AccountAdapter { displayName = itemView.findViewById(R.id.muted_user_display_name); unmute = itemView.findViewById(R.id.muted_user_unmute); muteNotifications = itemView.findViewById(R.id.muted_user_mute_notifications); - animateAvatar = PreferenceManager.getDefaultSharedPreferences(itemView.getContext()) - .getBoolean("animateGifAvatars", false); + animateAvatar = + PreferenceManager.getDefaultSharedPreferences(itemView.getContext()) + .getBoolean("animateGifAvatars", false); } void setupWithAccount(Account account, Boolean mutingNotifications) { id = account.getId(); - CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName); + CharSequence emojifiedName = + CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName); displayName.setText(emojifiedName); String format = username.getContext().getString(R.string.status_username_format); String formattedUsername = String.format(format, account.getUsername()); username.setText(formattedUsername); - int avatarRadius = avatar.getContext().getResources() - .getDimensionPixelSize(R.dimen.avatar_radius_48dp); + int avatarRadius = + avatar.getContext() + .getResources() + .getDimensionPixelSize(R.dimen.avatar_radius_48dp); ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius, animateAvatar); - String unmuteString = unmute.getContext().getString(R.string.action_unmute_desc, formattedUsername); + String unmuteString = + unmute.getContext().getString(R.string.action_unmute_desc, formattedUsername); unmute.setContentDescription(unmuteString); ViewCompat.setTooltipText(unmute, unmuteString); @@ -112,14 +118,21 @@ public class MutesAdapter extends AccountAdapter { if (notifications) { muteNotifications.setImageResource(R.drawable.ic_notifications_24dp); - String unmuteNotificationsString = muteNotifications.getContext() - .getString(R.string.action_unmute_notifications_desc, formattedUsername); + String unmuteNotificationsString = + muteNotifications + .getContext() + .getString( + R.string.action_unmute_notifications_desc, + formattedUsername); muteNotifications.setContentDescription(unmuteNotificationsString); ViewCompat.setTooltipText(muteNotifications, unmuteNotificationsString); } else { muteNotifications.setImageResource(R.drawable.ic_notifications_off_24dp); - String muteNotificationsString = muteNotifications.getContext() - .getString(R.string.action_mute_notifications_desc, formattedUsername); + String muteNotificationsString = + muteNotifications + .getContext() + .getString( + R.string.action_mute_notifications_desc, formattedUsername); muteNotifications.setContentDescription(muteNotificationsString); ViewCompat.setTooltipText(muteNotifications, muteNotificationsString); } @@ -128,7 +141,7 @@ public class MutesAdapter extends AccountAdapter { void setupActionListener(final AccountActionListener listener) { unmute.setOnClickListener(v -> listener.onMute(false, id, getAdapterPosition(), false)); muteNotifications.setOnClickListener( - v -> listener.onMute(true, id, getAdapterPosition(), !notifications)); + v -> listener.onMute(true, id, getAdapterPosition(), !notifications)); itemView.setOnClickListener(v -> listener.onViewAccount(id)); } } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java index 93a01a9..52e96a0 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -83,7 +83,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter { private static final int VIEW_TYPE_MOVE = 6; private static final int VIEW_TYPE_UNKNOWN = 7; - private static final InputFilter[] COLLAPSE_INPUT_FILTER = new InputFilter[]{SmartLengthInputFilter.INSTANCE}; + private static final InputFilter[] COLLAPSE_INPUT_FILTER = + new InputFilter[] {SmartLengthInputFilter.INSTANCE}; private static final InputFilter[] NO_INPUT_FILTER = new InputFilter[0]; private String accountId; @@ -93,12 +94,13 @@ public class NotificationsAdapter extends RecyclerView.Adapter { private AccountActionListener accountActionListener; private AdapterDataSource<NotificationViewData> dataSource; - public NotificationsAdapter(String accountId, - AdapterDataSource<NotificationViewData> dataSource, - StatusDisplayOptions statusDisplayOptions, - StatusActionListener statusListener, - NotificationActionListener notificationActionListener, - AccountActionListener accountActionListener) { + public NotificationsAdapter( + String accountId, + AdapterDataSource<NotificationViewData> dataSource, + StatusDisplayOptions statusDisplayOptions, + StatusActionListener statusListener, + NotificationActionListener notificationActionListener, + AccountActionListener accountActionListener) { this.accountId = accountId; this.dataSource = dataSource; @@ -113,51 +115,49 @@ public class NotificationsAdapter extends RecyclerView.Adapter { public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { LayoutInflater inflater = LayoutInflater.from(parent.getContext()); switch (viewType) { - case VIEW_TYPE_STATUS: { - View view = inflater - .inflate(R.layout.item_status, parent, false); - return new StatusViewHolder(view); - } - case VIEW_TYPE_MUTED_STATUS: { - View view = inflater - .inflate(R.layout.item_status_muted, parent, false); - return new MutedStatusViewHolder(view); - } - case VIEW_TYPE_STATUS_NOTIFICATION: { - View view = inflater - .inflate(R.layout.item_status_notification, parent, false); - return new StatusNotificationViewHolder(view, statusDisplayOptions); - } + case VIEW_TYPE_STATUS: + { + View view = inflater.inflate(R.layout.item_status, parent, false); + return new StatusViewHolder(view); + } + case VIEW_TYPE_MUTED_STATUS: + { + View view = inflater.inflate(R.layout.item_status_muted, parent, false); + return new MutedStatusViewHolder(view); + } + case VIEW_TYPE_STATUS_NOTIFICATION: + { + View view = inflater.inflate(R.layout.item_status_notification, parent, false); + return new StatusNotificationViewHolder(view, statusDisplayOptions); + } case VIEW_TYPE_MOVE: - case VIEW_TYPE_FOLLOW: { - View view = inflater - .inflate(R.layout.item_follow, parent, false); - return new FollowViewHolder(view, statusDisplayOptions); - } - case VIEW_TYPE_FOLLOW_REQUEST: { - ItemFollowRequestNotificationBinding binding = - ItemFollowRequestNotificationBinding.inflate( - LayoutInflater.from(parent.getContext()), parent, false - ); - return new FollowRequestViewHolder(binding, true); - } - case VIEW_TYPE_PLACEHOLDER: { - View view = inflater - .inflate(R.layout.item_status_placeholder, parent, false); - return new PlaceholderViewHolder(view); - } + case VIEW_TYPE_FOLLOW: + { + View view = inflater.inflate(R.layout.item_follow, parent, false); + return new FollowViewHolder(view, statusDisplayOptions); + } + case VIEW_TYPE_FOLLOW_REQUEST: + { + ItemFollowRequestNotificationBinding binding = + ItemFollowRequestNotificationBinding.inflate( + LayoutInflater.from(parent.getContext()), parent, false); + return new FollowRequestViewHolder(binding, true); + } + case VIEW_TYPE_PLACEHOLDER: + { + View view = inflater.inflate(R.layout.item_status_placeholder, parent, false); + return new PlaceholderViewHolder(view); + } default: - case VIEW_TYPE_UNKNOWN: { - View view = new View(parent.getContext()); - view.setLayoutParams( - new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - Utils.dpToPx(parent.getContext(), 24) - ) - ); - return new RecyclerView.ViewHolder(view) { - }; - } + case VIEW_TYPE_UNKNOWN: + { + View view = new View(parent.getContext()); + view.setLayoutParams( + new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + Utils.dpToPx(parent.getContext(), 24))); + return new RecyclerView.ViewHolder(view) {}; + } } } @@ -167,17 +167,20 @@ public class NotificationsAdapter extends RecyclerView.Adapter { } @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position, @NonNull List payloads) { + public void onBindViewHolder( + @NonNull RecyclerView.ViewHolder viewHolder, int position, @NonNull List payloads) { bindViewHolder(viewHolder, position, payloads); } - private void bindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position, @Nullable List payloads) { + private void bindViewHolder( + @NonNull RecyclerView.ViewHolder viewHolder, int position, @Nullable List payloads) { Object payloadForHolder = payloads != null && !payloads.isEmpty() ? payloads.get(0) : null; if (position < this.dataSource.getItemCount()) { NotificationViewData notification = dataSource.getItemAt(position); if (notification instanceof NotificationViewData.Placeholder) { if (payloadForHolder == null) { - NotificationViewData.Placeholder placeholder = ((NotificationViewData.Placeholder) notification); + NotificationViewData.Placeholder placeholder = + ((NotificationViewData.Placeholder) notification); PlaceholderViewHolder holder = (PlaceholderViewHolder) viewHolder; holder.setup(statusListener, placeholder.isLoading()); } @@ -186,83 +189,104 @@ public class NotificationsAdapter extends RecyclerView.Adapter { NotificationViewData.Concrete concreteNotificaton = (NotificationViewData.Concrete) notification; switch (viewHolder.getItemViewType()) { - case VIEW_TYPE_STATUS: { - StatusViewHolder holder = (StatusViewHolder) viewHolder; - StatusViewData.Concrete status = concreteNotificaton.getStatusViewData(); - holder.setupWithStatus(status, - statusListener, statusDisplayOptions, payloadForHolder); - if (concreteNotificaton.getType() == Notification.Type.POLL) { - holder.setPollInfo(accountId.equals(concreteNotificaton.getAccount().getId())); - } else { - holder.hideStatusInfo(); - } - break; - } - case VIEW_TYPE_MUTED_STATUS: { - MutedStatusViewHolder holder = (MutedStatusViewHolder) viewHolder; - StatusViewData.Concrete status = concreteNotificaton.getStatusViewData(); - holder.setupWithStatus(status, - statusListener, statusDisplayOptions, payloadForHolder); - break; - } - case VIEW_TYPE_STATUS_NOTIFICATION: { - StatusNotificationViewHolder holder = (StatusNotificationViewHolder) viewHolder; - StatusViewData.Concrete statusViewData = concreteNotificaton.getStatusViewData(); - if (payloadForHolder == null) { - if (statusViewData == null) { - holder.showNotificationContent(false); + case VIEW_TYPE_STATUS: + { + StatusViewHolder holder = (StatusViewHolder) viewHolder; + StatusViewData.Concrete status = concreteNotificaton.getStatusViewData(); + holder.setupWithStatus( + status, statusListener, statusDisplayOptions, payloadForHolder); + if (concreteNotificaton.getType() == Notification.Type.POLL) { + holder.setPollInfo( + accountId.equals(concreteNotificaton.getAccount().getId())); } else { - holder.showNotificationContent(true); - - holder.setDisplayName(statusViewData.getUserFullName(), statusViewData.getAccountEmojis()); - holder.setUsername(statusViewData.getNickname()); - holder.setCreatedAt(statusViewData.getCreatedAt()); - - if(concreteNotificaton.getType() == Notification.Type.STATUS) { - holder.setAvatar(statusViewData.getAvatar(), statusViewData.isBot()); + holder.hideStatusInfo(); + } + break; + } + case VIEW_TYPE_MUTED_STATUS: + { + MutedStatusViewHolder holder = (MutedStatusViewHolder) viewHolder; + StatusViewData.Concrete status = concreteNotificaton.getStatusViewData(); + holder.setupWithStatus( + status, statusListener, statusDisplayOptions, payloadForHolder); + break; + } + case VIEW_TYPE_STATUS_NOTIFICATION: + { + StatusNotificationViewHolder holder = + (StatusNotificationViewHolder) viewHolder; + StatusViewData.Concrete statusViewData = + concreteNotificaton.getStatusViewData(); + if (payloadForHolder == null) { + if (statusViewData == null) { + holder.showNotificationContent(false); } else { - holder.setAvatars(statusViewData.getAvatar(), - concreteNotificaton.getAccount().getAvatar()); + holder.showNotificationContent(true); + + holder.setDisplayName( + statusViewData.getUserFullName(), + statusViewData.getAccountEmojis()); + holder.setUsername(statusViewData.getNickname()); + holder.setCreatedAt(statusViewData.getCreatedAt()); + + if (concreteNotificaton.getType() == Notification.Type.STATUS) { + holder.setAvatar( + statusViewData.getAvatar(), statusViewData.isBot()); + } else { + holder.setAvatars( + statusViewData.getAvatar(), + concreteNotificaton.getAccount().getAvatar()); + } } - } - holder.setMessage(concreteNotificaton, statusListener); - holder.setupButtons(notificationActionListener, - concreteNotificaton.getAccount().getId(), - concreteNotificaton.getId()); - } else { - if (payloadForHolder instanceof List) - for (Object item : (List) payloadForHolder) { - if (StatusBaseViewHolder.Key.KEY_CREATED.equals(item) && statusViewData != null) { - holder.setCreatedAt(statusViewData.getCreatedAt()); + holder.setMessage(concreteNotificaton, statusListener); + holder.setupButtons( + notificationActionListener, + concreteNotificaton.getAccount().getId(), + concreteNotificaton.getId()); + } else { + if (payloadForHolder instanceof List) + for (Object item : (List) payloadForHolder) { + if (StatusBaseViewHolder.Key.KEY_CREATED.equals(item) + && statusViewData != null) { + holder.setCreatedAt(statusViewData.getCreatedAt()); + } } - } + } + break; } - break; - } - case VIEW_TYPE_FOLLOW: { - if (payloadForHolder == null) { - FollowViewHolder holder = (FollowViewHolder) viewHolder; - holder.setMessage(concreteNotificaton.getAccount(), null); - holder.setupButtons(notificationActionListener, concreteNotificaton.getAccount().getId()); + case VIEW_TYPE_FOLLOW: + { + if (payloadForHolder == null) { + FollowViewHolder holder = (FollowViewHolder) viewHolder; + holder.setMessage(concreteNotificaton.getAccount(), null); + holder.setupButtons( + notificationActionListener, + concreteNotificaton.getAccount().getId()); + } + break; } - break; - } - case VIEW_TYPE_MOVE: { - if (payloadForHolder == null) { - FollowViewHolder holder = (FollowViewHolder) viewHolder; - holder.setMessage(concreteNotificaton.getTarget(), concreteNotificaton.getAccount()); - holder.setupButtons(notificationActionListener, concreteNotificaton.getAccount().getId()); + case VIEW_TYPE_MOVE: + { + if (payloadForHolder == null) { + FollowViewHolder holder = (FollowViewHolder) viewHolder; + holder.setMessage( + concreteNotificaton.getTarget(), + concreteNotificaton.getAccount()); + holder.setupButtons( + notificationActionListener, + concreteNotificaton.getAccount().getId()); + } + break; } - break; - } - case VIEW_TYPE_FOLLOW_REQUEST: { - if (payloadForHolder == null) { - FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder; - holder.setupWithAccount(concreteNotificaton.getAccount()); - holder.setupActionListener(accountActionListener); + case VIEW_TYPE_FOLLOW_REQUEST: + { + if (payloadForHolder == null) { + FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder; + holder.setupWithAccount(concreteNotificaton.getAccount()); + holder.setupActionListener(accountActionListener); + } } - } default: } } @@ -274,17 +298,17 @@ public class NotificationsAdapter extends RecyclerView.Adapter { } public void setMediaPreviewEnabled(boolean mediaPreviewEnabled) { - this.statusDisplayOptions = statusDisplayOptions.copy( - statusDisplayOptions.animateAvatars(), - mediaPreviewEnabled, - statusDisplayOptions.useAbsoluteTime(), - statusDisplayOptions.showBotOverlay(), - statusDisplayOptions.useBlurhash(), - CardViewMode.NONE, - statusDisplayOptions.confirmReblogs(), - statusDisplayOptions.renderStatusAsMention(), - statusDisplayOptions.hideStats() - ); + this.statusDisplayOptions = + statusDisplayOptions.copy( + statusDisplayOptions.animateAvatars(), + mediaPreviewEnabled, + statusDisplayOptions.useAbsoluteTime(), + statusDisplayOptions.showBotOverlay(), + statusDisplayOptions.useBlurhash(), + CardViewMode.NONE, + statusDisplayOptions.confirmReblogs(), + statusDisplayOptions.renderStatusAsMention(), + statusDisplayOptions.hideStats()); } public boolean isMediaPreviewEnabled() { @@ -298,43 +322,49 @@ public class NotificationsAdapter extends RecyclerView.Adapter { NotificationViewData.Concrete concrete = ((NotificationViewData.Concrete) notification); switch (concrete.getType()) { case MENTION: - case POLL: { - if (concrete.getStatusViewData() != null && concrete.getStatusViewData().isMuted()) - return VIEW_TYPE_MUTED_STATUS; - return VIEW_TYPE_STATUS; - } + case POLL: + { + if (concrete.getStatusViewData() != null + && concrete.getStatusViewData().isMuted()) + return VIEW_TYPE_MUTED_STATUS; + return VIEW_TYPE_STATUS; + } case STATUS: if (statusDisplayOptions.renderStatusAsMention()) { - if (concrete.getStatusViewData() != null && concrete.getStatusViewData().isMuted()) + if (concrete.getStatusViewData() != null + && concrete.getStatusViewData().isMuted()) return VIEW_TYPE_MUTED_STATUS; return VIEW_TYPE_STATUS; } /* fallthrough */ case FAVOURITE: case REBLOG: - case EMOJI_REACTION: { - return VIEW_TYPE_STATUS_NOTIFICATION; - } - case FOLLOW: { - return VIEW_TYPE_FOLLOW; - } - case FOLLOW_REQUEST: { - return VIEW_TYPE_FOLLOW_REQUEST; - } - case MOVE: { - return VIEW_TYPE_MOVE; - } - default: { - return VIEW_TYPE_UNKNOWN; - } + case EMOJI_REACTION: + { + return VIEW_TYPE_STATUS_NOTIFICATION; + } + case FOLLOW: + { + return VIEW_TYPE_FOLLOW; + } + case FOLLOW_REQUEST: + { + return VIEW_TYPE_FOLLOW_REQUEST; + } + case MOVE: + { + return VIEW_TYPE_MOVE; + } + default: + { + return VIEW_TYPE_UNKNOWN; + } } } else if (notification instanceof NotificationViewData.Placeholder) { return VIEW_TYPE_PLACEHOLDER; } else { throw new AssertionError("Unknown notification type"); } - - } public interface NotificationActionListener { @@ -345,11 +375,11 @@ public class NotificationsAdapter extends RecyclerView.Adapter { void onExpandedChange(boolean expanded, int position); /** - * Called when the status {@link android.widget.ToggleButton} responsible for collapsing long - * status content is interacted with. + * Called when the status {@link android.widget.ToggleButton} responsible for collapsing + * long status content is interacted with. * * @param isCollapsed Whether the status content is shown in a collapsed state or fully. - * @param position The position of the status in the list. + * @param position The position of the status in the list. */ void onNotificationContentCollapsedChange(boolean isCollapsed, int position); @@ -379,17 +409,19 @@ public class NotificationsAdapter extends RecyclerView.Adapter { Drawable drawable; CharSequence emojifiedMessage; - if(from != null) { + if (from != null) { String format = context.getString(R.string.notification_move_format); String wrappedFromName = StringUtils.unicodeWrap(from.getName()); String wholeMessage = String.format(format, wrappedFromName); - emojifiedMessage = CustomEmojiHelper.emojify(wholeMessage, from.getEmojis(), message, true); + emojifiedMessage = + CustomEmojiHelper.emojify(wholeMessage, from.getEmojis(), message, true); drawable = ContextCompat.getDrawable(context, R.drawable.ic_reply_24dp); } else { String format = context.getString(R.string.notification_follow_format); String wholeMessage = String.format(format, wrappedDisplayName); - emojifiedMessage = CustomEmojiHelper.emojify(wholeMessage, account.getEmojis(), message, true); + emojifiedMessage = + CustomEmojiHelper.emojify(wholeMessage, account.getEmojis(), message, true); drawable = ContextCompat.getDrawable(context, R.drawable.ic_person_add_24dp); } @@ -398,19 +430,26 @@ public class NotificationsAdapter extends RecyclerView.Adapter { message.setText(emojifiedMessage); - String username = context.getString(R.string.status_username_format, account.getUsername()); + String username = + context.getString(R.string.status_username_format, account.getUsername()); usernameView.setText(username); - CharSequence emojifiedDisplayName = CustomEmojiHelper.emojify(wrappedDisplayName, account.getEmojis(), usernameView, true); + CharSequence emojifiedDisplayName = + CustomEmojiHelper.emojify( + wrappedDisplayName, account.getEmojis(), usernameView, true); displayNameView.setText(emojifiedDisplayName); - int avatarRadius = avatar.getContext().getResources() - .getDimensionPixelSize(R.dimen.avatar_radius_42dp); + int avatarRadius = + avatar.getContext() + .getResources() + .getDimensionPixelSize(R.dimen.avatar_radius_42dp); - ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius, + ImageLoadingHelper.loadAvatar( + account.getAvatar(), + avatar, + avatarRadius, statusDisplayOptions.animateAvatars()); - } void setupButtons(final NotificationActionListener listener, final String accountId) { @@ -418,7 +457,6 @@ public class NotificationsAdapter extends RecyclerView.Adapter { } } - private static class StatusNotificationViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { private final TextView message; @@ -432,7 +470,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter { private final TextView replyInfo; private final TextView contentWarningDescriptionTextView; private final Button contentWarningButton; - private final Button contentCollapseButton; // TODO: This code SHOULD be based on StatusBaseViewHolder + private final Button + contentCollapseButton; // TODO: This code SHOULD be based on StatusBaseViewHolder private StatusDisplayOptions statusDisplayOptions; private String accountId; @@ -457,7 +496,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter { statusAvatar = itemView.findViewById(R.id.notification_status_avatar); notificationAvatar = itemView.findViewById(R.id.notification_notification_avatar); replyInfo = itemView.findViewById(R.id.notification_reply_info); - contentWarningDescriptionTextView = itemView.findViewById(R.id.notification_content_warning_description); + contentWarningDescriptionTextView = + itemView.findViewById(R.id.notification_content_warning_description); contentWarningButton = itemView.findViewById(R.id.notification_content_warning_button); contentCollapseButton = itemView.findViewById(R.id.button_toggle_notification_content); this.statusDisplayOptions = statusDisplayOptions; @@ -472,9 +512,18 @@ public class NotificationsAdapter extends RecyclerView.Adapter { shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()); longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault()); - this.avatarRadius48dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_48dp); - this.avatarRadius36dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_36dp); - this.avatarRadius24dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_24dp); + this.avatarRadius48dp = + itemView.getContext() + .getResources() + .getDimensionPixelSize(R.dimen.avatar_radius_48dp); + this.avatarRadius36dp = + itemView.getContext() + .getResources() + .getDimensionPixelSize(R.dimen.avatar_radius_36dp); + this.avatarRadius24dp = + itemView.getContext() + .getResources() + .getDimensionPixelSize(R.dimen.avatar_radius_24dp); } private void showNotificationContent(boolean show) { @@ -520,10 +569,15 @@ public class NotificationsAdapter extends RecyclerView.Adapter { if (createdAt != null) { long then = createdAt.getTime(); long now = new Date().getTime(); - readout = TimestampUtils.getRelativeTimeSpanString(timestampInfo.getContext(), then, now); - readoutAloud = android.text.format.DateUtils.getRelativeTimeSpanString(then, now, - android.text.format.DateUtils.SECOND_IN_MILLIS, - android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE); + readout = + TimestampUtils.getRelativeTimeSpanString( + timestampInfo.getContext(), then, now); + readoutAloud = + android.text.format.DateUtils.getRelativeTimeSpanString( + then, + now, + android.text.format.DateUtils.SECOND_IN_MILLIS, + android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE); } else { // unknown minutes~ readout = "?m"; @@ -537,7 +591,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter { void setMessage(NotificationViewData.Concrete notificationViewData, LinkListener listener) { this.statusViewData = notificationViewData.getStatusViewData(); - String displayName = StringUtils.unicodeWrap(notificationViewData.getAccount().getName()); + String displayName = + StringUtils.unicodeWrap(notificationViewData.getAccount().getName()); Notification.Type type = notificationViewData.getType(); Context context = message.getContext(); @@ -545,73 +600,91 @@ public class NotificationsAdapter extends RecyclerView.Adapter { SpannableStringBuilder builder = new SpannableStringBuilder(); switch (type) { default: - case FAVOURITE: { - icon = ContextCompat.getDrawable(context, R.drawable.ic_star_24dp); - if (icon != null) { - icon.setColorFilter(ContextCompat.getColor(context, - R.color.tusky_orange), PorterDuff.Mode.SRC_ATOP); - } + case FAVOURITE: + { + icon = ContextCompat.getDrawable(context, R.drawable.ic_star_24dp); + if (icon != null) { + icon.setColorFilter( + ContextCompat.getColor(context, R.color.tusky_orange), + PorterDuff.Mode.SRC_ATOP); + } - String format = context.getString(R.string.notification_favourite_format); - builder.append(String.format(format, displayName)); - break; - } - case REBLOG: { - icon = ContextCompat.getDrawable(context, R.drawable.ic_repeat_24dp); - if (icon != null) { - icon.setColorFilter(ContextCompat.getColor(context, - R.color.tusky_blue), PorterDuff.Mode.SRC_ATOP); + String format = context.getString(R.string.notification_favourite_format); + builder.append(String.format(format, displayName)); + break; } + case REBLOG: + { + icon = ContextCompat.getDrawable(context, R.drawable.ic_repeat_24dp); + if (icon != null) { + icon.setColorFilter( + ContextCompat.getColor(context, R.color.tusky_blue), + PorterDuff.Mode.SRC_ATOP); + } - String format = context.getString(R.string.notification_reblog_format); - builder.append(String.format(format, displayName)); - break; - } - case STATUS: { - icon = ContextCompat.getDrawable(context, R.drawable.ic_home_24dp); - if (icon != null) { - icon.setColorFilter(ContextCompat.getColor(context, - R.color.tusky_blue), PorterDuff.Mode.SRC_ATOP); + String format = context.getString(R.string.notification_reblog_format); + builder.append(String.format(format, displayName)); + break; } + case STATUS: + { + icon = ContextCompat.getDrawable(context, R.drawable.ic_home_24dp); + if (icon != null) { + icon.setColorFilter( + ContextCompat.getColor(context, R.color.tusky_blue), + PorterDuff.Mode.SRC_ATOP); + } - String format = context.getString(R.string.notification_subscription_format); - builder.append(String.format(format, displayName)); - break; - } - case EMOJI_REACTION: { - icon = ContextCompat.getDrawable(context, R.drawable.ic_emoji_24dp); - if(icon != null) { - icon.setColorFilter(ContextCompat.getColor(context, - R.color.tusky_green), PorterDuff.Mode.SRC_ATOP); + String format = + context.getString(R.string.notification_subscription_format); + builder.append(String.format(format, displayName)); + break; } + case EMOJI_REACTION: + { + icon = ContextCompat.getDrawable(context, R.drawable.ic_emoji_24dp); + if (icon != null) { + icon.setColorFilter( + ContextCompat.getColor(context, R.color.tusky_green), + PorterDuff.Mode.SRC_ATOP); + } - String format = context.getString(R.string.notification_emoji_format); - String emojiCode = notificationViewData.getEmoji(); - builder.append(String.format(format, displayName, emojiCode)); - - final String emojiUrl = notificationViewData.getEmojiUrl(); - if(emojiUrl != null) { - // terrible hack... ideally, there should be a CharSequence formatter - final int emojiPosition = format.indexOf("%s", 1) - - "%s".length() + displayName.length(); + String format = context.getString(R.string.notification_emoji_format); + String emojiCode = notificationViewData.getEmoji(); + builder.append(String.format(format, displayName, emojiCode)); + + final String emojiUrl = notificationViewData.getEmojiUrl(); + if (emojiUrl != null) { + // terrible hack... ideally, there should be a CharSequence formatter + final int emojiPosition = + format.indexOf("%s", 1) - "%s".length() + displayName.length(); + + var span = CustomEmojiHelper.createEmojiSpan(emojiUrl, message, true); + builder.setSpan( + span, + emojiPosition, + emojiPosition + emojiCode.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } - var span = CustomEmojiHelper.createEmojiSpan(emojiUrl, message, true); - builder.setSpan(span, emojiPosition, emojiPosition + emojiCode.length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + break; } - - break; - } } message.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null); - builder.setSpan(new StyleSpan(Typeface.BOLD), 0, displayName.length(), + builder.setSpan( + new StyleSpan(Typeface.BOLD), + 0, + displayName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - CharSequence emojifiedText = CustomEmojiHelper.emojify(builder, notificationViewData.getAccount().getEmojis(), message, true); + CharSequence emojifiedText = + CustomEmojiHelper.emojify( + builder, notificationViewData.getAccount().getEmojis(), message, true); message.setText(emojifiedText); if (statusViewData != null) { boolean hasSpoiler = !TextUtils.isEmpty(statusViewData.getSpoilerText()); - contentWarningDescriptionTextView.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE); + contentWarningDescriptionTextView.setVisibility( + hasSpoiler ? View.VISIBLE : View.GONE); contentWarningButton.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE); if (statusViewData.isExpanded()) { contentWarningButton.setText(R.string.status_content_warning_show_less); @@ -619,21 +692,25 @@ public class NotificationsAdapter extends RecyclerView.Adapter { contentWarningButton.setText(R.string.status_content_warning_show_more); } - contentWarningButton.setOnClickListener(view -> { - if (getAdapterPosition() != RecyclerView.NO_POSITION) { - notificationActionListener.onExpandedChange(!statusViewData.isExpanded(), getAdapterPosition()); - } - statusContent.setVisibility(statusViewData.isExpanded() ? View.GONE : View.VISIBLE); - }); + contentWarningButton.setOnClickListener( + view -> { + if (getAdapterPosition() != RecyclerView.NO_POSITION) { + notificationActionListener.onExpandedChange( + !statusViewData.isExpanded(), getAdapterPosition()); + } + statusContent.setVisibility( + statusViewData.isExpanded() ? View.GONE : View.VISIBLE); + }); setupContentAndSpoiler(listener); setupReplyInfo(); } - } - void setupButtons(final NotificationActionListener listener, final String accountId, - final String notificationId) { + void setupButtons( + final NotificationActionListener listener, + final String accountId, + final String notificationId) { this.notificationActionListener = listener; this.accountId = accountId; this.notificationId = notificationId; @@ -642,8 +719,11 @@ public class NotificationsAdapter extends RecyclerView.Adapter { void setAvatar(@Nullable String statusAvatarUrl, boolean isBot) { statusAvatar.setPaddingRelative(0, 0, 0, 0); - ImageLoadingHelper.loadAvatar(statusAvatarUrl, - statusAvatar, avatarRadius48dp, statusDisplayOptions.animateAvatars()); + ImageLoadingHelper.loadAvatar( + statusAvatarUrl, + statusAvatar, + avatarRadius48dp, + statusDisplayOptions.animateAvatars()); if (statusDisplayOptions.showBotOverlay() && isBot) { notificationAvatar.setVisibility(View.VISIBLE); @@ -661,12 +741,18 @@ public class NotificationsAdapter extends RecyclerView.Adapter { int padding = Utils.dpToPx(statusAvatar.getContext(), 12); statusAvatar.setPaddingRelative(0, 0, padding, padding); - ImageLoadingHelper.loadAvatar(statusAvatarUrl, - statusAvatar, avatarRadius36dp, statusDisplayOptions.animateAvatars()); + ImageLoadingHelper.loadAvatar( + statusAvatarUrl, + statusAvatar, + avatarRadius36dp, + statusDisplayOptions.animateAvatars()); notificationAvatar.setVisibility(View.VISIBLE); - ImageLoadingHelper.loadAvatar(notificationAvatarUrl, notificationAvatar, - avatarRadius24dp, statusDisplayOptions.animateAvatars()); + ImageLoadingHelper.loadAvatar( + notificationAvatarUrl, + notificationAvatar, + avatarRadius24dp, + statusDisplayOptions.animateAvatars()); } @Override @@ -697,12 +783,15 @@ public class NotificationsAdapter extends RecyclerView.Adapter { List<Emoji> emojis = statusViewData.getStatusEmojis(); if (statusViewData.isCollapsible() && (statusViewData.isExpanded() || !hasSpoiler)) { - contentCollapseButton.setOnClickListener(view -> { - int position = getAdapterPosition(); - if (position != RecyclerView.NO_POSITION && notificationActionListener != null) { - notificationActionListener.onNotificationContentCollapsedChange(!statusViewData.isCollapsed(), position); - } - }); + contentCollapseButton.setOnClickListener( + view -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION + && notificationActionListener != null) { + notificationActionListener.onNotificationContentCollapsedChange( + !statusViewData.isCollapsed(), position); + } + }); contentCollapseButton.setVisibility(View.VISIBLE); if (statusViewData.isCollapsed()) { @@ -718,10 +807,14 @@ public class NotificationsAdapter extends RecyclerView.Adapter { } CharSequence emojifiedText = CustomEmojiHelper.emojify(content, emojis, statusContent); - LinkHelper.setClickableText(statusContent, emojifiedText, statusViewData.getMentions(), listener); + LinkHelper.setClickableText( + statusContent, emojifiedText, statusViewData.getMentions(), listener); CharSequence emojifiedContentWarning = - CustomEmojiHelper.emojify(statusViewData.getSpoilerText(), statusViewData.getStatusEmojis(), contentWarningDescriptionTextView); + CustomEmojiHelper.emojify( + statusViewData.getSpoilerText(), + statusViewData.getStatusEmojis(), + contentWarningDescriptionTextView); contentWarningDescriptionTextView.setText(emojifiedContentWarning); } @@ -729,13 +822,17 @@ public class NotificationsAdapter extends RecyclerView.Adapter { if (statusViewData.getInReplyToId() != null) { Context context = replyInfo.getContext(); String replyToAccount = statusViewData.getInReplyToAccountAcct(); - replyInfo.setText(context.getString(R.string.status_replied_to_format, replyToAccount)); + replyInfo.setText( + context.getString(R.string.status_replied_to_format, replyToAccount)); if (!statusViewData.getParentVisible()) { - replyInfo.setPaintFlags(replyInfo.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); + replyInfo.setPaintFlags( + replyInfo.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); replyInfo.setOnClickListener(null); } else { - replyInfo.setPaintFlags(replyInfo.getPaintFlags() & (~Paint.STRIKE_THRU_TEXT_FLAG)); - replyInfo.setOnClickListener(v -> notificationActionListener.onViewReplyTo(getAdapterPosition())); + replyInfo.setPaintFlags( + replyInfo.getPaintFlags() & (~Paint.STRIKE_THRU_TEXT_FLAG)); + replyInfo.setOnClickListener( + v -> notificationActionListener.onViewReplyTo(getAdapterPosition())); } replyInfo.setVisibility(View.VISIBLE); } else { diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.java b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.java index 403fc7d..ef5991a 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.java @@ -15,11 +15,10 @@ package com.keylesspalace.tusky.adapter; -import androidx.recyclerview.widget.RecyclerView; import android.view.View; import android.widget.Button; import android.widget.ProgressBar; - +import androidx.recyclerview.widget.RecyclerView; import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.interfaces.ChatActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener; @@ -44,17 +43,19 @@ public final class PlaceholderViewHolder extends RecyclerView.ViewHolder { public void setup(final StatusActionListener listener, boolean progress) { setup(progress); - loadMoreButton.setOnClickListener(v -> { - loadMoreButton.setEnabled(false); - listener.onLoadMore(getAdapterPosition()); - }); + loadMoreButton.setOnClickListener( + v -> { + loadMoreButton.setEnabled(false); + listener.onLoadMore(getAdapterPosition()); + }); } public void setup(final ChatActionListener listener, boolean progress) { setup(progress); - loadMoreButton.setOnClickListener( v -> { - loadMoreButton.setEnabled(false); - listener.onLoadMore(getAdapterPosition()); - }); + loadMoreButton.setOnClickListener( + v -> { + loadMoreButton.setEnabled(false); + listener.onLoadMore(getAdapterPosition()); + }); } -}
\ No newline at end of file +} diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/SavedTootAdapter.java b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/SavedTootAdapter.java index 6d4889c..28a745b 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/SavedTootAdapter.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/SavedTootAdapter.java @@ -16,17 +16,15 @@ package com.keylesspalace.tusky.adapter; import android.content.Context; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.TextView; - +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.db.TootEntity; - import java.util.ArrayList; import java.util.List; @@ -42,8 +40,9 @@ public class SavedTootAdapter extends RecyclerView.Adapter { @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_saved_toot, parent, false); + View view = + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_saved_toot, parent, false); return new TootViewHolder(view); } @@ -111,10 +110,11 @@ public class SavedTootAdapter extends RecyclerView.Adapter { if (item != null) { content.setText(item.getText()); - suppr.setOnClickListener(v -> { - v.setEnabled(false); - handler.delete(getAdapterPosition(), item); - }); + suppr.setOnClickListener( + v -> { + v.setEnabled(false); + handler.delete(getAdapterPosition(), item); + }); view.setOnClickListener(v -> handler.click(getAdapterPosition(), item)); } } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/SingleViewHolder.java b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/SingleViewHolder.java index 26dfbb6..4c050a7 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/SingleViewHolder.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/SingleViewHolder.java @@ -1,11 +1,11 @@ package com.keylesspalace.tusky.adapter; -import androidx.recyclerview.widget.RecyclerView; import android.view.View; +import androidx.recyclerview.widget.RecyclerView; // empty class to be able to instantiate ViewHolder which is abstract for dumbass reason public class SingleViewHolder extends RecyclerView.ViewHolder { - public SingleViewHolder(View view) { - super(view); - } -} + public SingleViewHolder(View view) { + super(view); + } +} diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java index 67c10c1..ec3132b 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -1,24 +1,24 @@ package com.keylesspalace.tusky.adapter; +import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription; + import android.content.Context; +import android.graphics.Paint; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.text.Spanned; import android.text.TextUtils; import android.text.format.DateUtils; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; -import android.view.MotionEvent; import android.widget.Button; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; -import android.util.Log; -import android.graphics.Paint; - import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -27,42 +27,37 @@ import androidx.core.text.HtmlCompat; import androidx.recyclerview.widget.DefaultItemAnimator; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; - +import at.connyduck.sparkbutton.SparkButton; +import at.connyduck.sparkbutton.helpers.Utils; 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.google.android.flexbox.FlexboxLayoutManager; +import com.google.android.material.button.MaterialButton; import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.entity.Attachment; import com.keylesspalace.tusky.entity.Attachment.Focus; import com.keylesspalace.tusky.entity.Attachment.MetaData; import com.keylesspalace.tusky.entity.Card; import com.keylesspalace.tusky.entity.Emoji; -import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.EmojiReaction; +import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.util.*; -import com.keylesspalace.tusky.view.MediaPreviewImageView; import com.keylesspalace.tusky.view.EmojiKeyboard; +import com.keylesspalace.tusky.view.MediaPreviewImageView; import com.keylesspalace.tusky.viewdata.PollOptionViewData; import com.keylesspalace.tusky.viewdata.PollViewData; import com.keylesspalace.tusky.viewdata.PollViewDataKt; import com.keylesspalace.tusky.viewdata.StatusViewData; - import java.text.NumberFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import java.util.Locale; - -import at.connyduck.sparkbutton.SparkButton; -import at.connyduck.sparkbutton.helpers.Utils; import kotlin.collections.CollectionsKt; -import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription; - public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { public static class Key { public static final String KEY_CREATED = "created"; @@ -144,26 +139,29 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { itemView.findViewById(R.id.status_media_preview_container).setClipToOutline(true); - mediaPreviews = new MediaPreviewImageView[]{ - itemView.findViewById(R.id.status_media_preview_0), - itemView.findViewById(R.id.status_media_preview_1), - itemView.findViewById(R.id.status_media_preview_2), - itemView.findViewById(R.id.status_media_preview_3) - }; - mediaOverlays = new ImageView[]{ - itemView.findViewById(R.id.status_media_overlay_0), - itemView.findViewById(R.id.status_media_overlay_1), - itemView.findViewById(R.id.status_media_overlay_2), - itemView.findViewById(R.id.status_media_overlay_3) - }; + mediaPreviews = + new MediaPreviewImageView[] { + itemView.findViewById(R.id.status_media_preview_0), + itemView.findViewById(R.id.status_media_preview_1), + itemView.findViewById(R.id.status_media_preview_2), + itemView.findViewById(R.id.status_media_preview_3) + }; + mediaOverlays = + new ImageView[] { + itemView.findViewById(R.id.status_media_overlay_0), + itemView.findViewById(R.id.status_media_overlay_1), + itemView.findViewById(R.id.status_media_overlay_2), + itemView.findViewById(R.id.status_media_overlay_3) + }; sensitiveMediaWarning = itemView.findViewById(R.id.status_sensitive_media_warning); sensitiveMediaShow = itemView.findViewById(R.id.status_sensitive_media_button); - mediaLabels = new TextView[]{ - itemView.findViewById(R.id.status_media_label_0), - itemView.findViewById(R.id.status_media_label_1), - itemView.findViewById(R.id.status_media_label_2), - itemView.findViewById(R.id.status_media_label_3) - }; + mediaLabels = + new TextView[] { + itemView.findViewById(R.id.status_media_label_0), + itemView.findViewById(R.id.status_media_label_1), + itemView.findViewById(R.id.status_media_label_2), + itemView.findViewById(R.id.status_media_label_3) + }; mediaDescriptions = new CharSequence[mediaLabels.length]; contentWarningDescription = itemView.findViewById(R.id.status_content_warning_description); contentWarningButton = itemView.findViewById(R.id.status_content_warning_button); @@ -188,17 +186,29 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { this.shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()); this.longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault()); - this.avatarRadius48dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_48dp); - this.avatarRadius36dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_36dp); - this.avatarRadius24dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_24dp); - - mediaPreviewUnloaded = new ColorDrawable(ThemeUtils.getColor(itemView.getContext(), R.attr.colorBackgroundAccent)); + this.avatarRadius48dp = + itemView.getContext() + .getResources() + .getDimensionPixelSize(R.dimen.avatar_radius_48dp); + this.avatarRadius36dp = + itemView.getContext() + .getResources() + .getDimensionPixelSize(R.dimen.avatar_radius_36dp); + this.avatarRadius24dp = + itemView.getContext() + .getResources() + .getDimensionPixelSize(R.dimen.avatar_radius_24dp); + + mediaPreviewUnloaded = + new ColorDrawable( + ThemeUtils.getColor(itemView.getContext(), R.attr.colorBackgroundAccent)); } protected abstract int getMediaPreviewHeight(Context context); protected void setDisplayName(String name, List<Emoji> customEmojis) { - CharSequence emojifiedName = CustomEmojiHelper.emojify(name, customEmojis, displayName, true); + CharSequence emojifiedName = + CustomEmojiHelper.emojify(name, customEmojis, displayName, true); displayName.setText(emojifiedName); } @@ -212,35 +222,62 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { contentWarningButton.performClick(); } - protected void setSpoilerAndContent(boolean expanded, - @NonNull Spanned content, - @Nullable String spoilerText, - @Nullable Status.Mention[] mentions, - @NonNull List<Emoji> emojis, - @Nullable PollViewData poll, - @NonNull StatusDisplayOptions statusDisplayOptions, - final StatusActionListener listener) { + protected void setSpoilerAndContent( + boolean expanded, + @NonNull Spanned content, + @Nullable String spoilerText, + @Nullable Status.Mention[] mentions, + @NonNull List<Emoji> emojis, + @Nullable PollViewData poll, + @NonNull StatusDisplayOptions statusDisplayOptions, + final StatusActionListener listener) { boolean sensitive = !TextUtils.isEmpty(spoilerText); if (sensitive) { - CharSequence emojiSpoiler = CustomEmojiHelper.emojify(spoilerText, emojis, contentWarningDescription); + CharSequence emojiSpoiler = + CustomEmojiHelper.emojify(spoilerText, emojis, contentWarningDescription); contentWarningDescription.setText(emojiSpoiler); contentWarningDescription.setVisibility(View.VISIBLE); contentWarningButton.setVisibility(View.VISIBLE); setContentWarningButtonText(expanded); - contentWarningButton.setOnClickListener(view -> { - contentWarningDescription.invalidate(); - if (getAdapterPosition() != RecyclerView.NO_POSITION) { - listener.onExpandedChange(!expanded, getAdapterPosition()); - } - setContentWarningButtonText(!expanded); - - this.setTextVisible(sensitive, !expanded, content, mentions, emojis, poll, statusDisplayOptions, listener); - }); - this.setTextVisible(sensitive, expanded, content, mentions, emojis, poll, statusDisplayOptions, listener); + contentWarningButton.setOnClickListener( + view -> { + contentWarningDescription.invalidate(); + if (getAdapterPosition() != RecyclerView.NO_POSITION) { + listener.onExpandedChange(!expanded, getAdapterPosition()); + } + setContentWarningButtonText(!expanded); + + this.setTextVisible( + sensitive, + !expanded, + content, + mentions, + emojis, + poll, + statusDisplayOptions, + listener); + }); + this.setTextVisible( + sensitive, + expanded, + content, + mentions, + emojis, + poll, + statusDisplayOptions, + listener); } else { contentWarningDescription.setVisibility(View.GONE); contentWarningButton.setVisibility(View.GONE); - this.setTextVisible(sensitive, true, content, mentions, emojis, poll, statusDisplayOptions, listener); + this.setTextVisible( + sensitive, + true, + content, + mentions, + emojis, + poll, + statusDisplayOptions, + listener); } } @@ -252,14 +289,15 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { } } - private void setTextVisible(boolean sensitive, - boolean expanded, - Spanned content, - Status.Mention[] mentions, - List<Emoji> emojis, - @Nullable PollViewData poll, - StatusDisplayOptions statusDisplayOptions, - final StatusActionListener listener) { + private void setTextVisible( + boolean sensitive, + boolean expanded, + Spanned content, + Status.Mention[] mentions, + List<Emoji> emojis, + @Nullable PollViewData poll, + StatusDisplayOptions statusDisplayOptions, + final StatusActionListener listener) { if (expanded) { CharSequence emojifiedText = CustomEmojiHelper.emojify(content, emojis, this.content); LinkHelper.setClickableText(this.content, emojifiedText, mentions, listener); @@ -288,10 +326,11 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { pollOptions.setVisibility(View.GONE); } - private void setAvatar(String url, - @Nullable String rebloggedUrl, - boolean isBot, - StatusDisplayOptions statusDisplayOptions) { + private void setAvatar( + String url, + @Nullable String rebloggedUrl, + boolean isBot, + StatusDisplayOptions statusDisplayOptions) { int avatarRadius; if (TextUtils.isEmpty(rebloggedUrl)) { @@ -300,9 +339,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { if (statusDisplayOptions.showBotOverlay() && isBot) { avatarInset.setVisibility(View.VISIBLE); avatarInset.setBackgroundColor(0x50ffffff); - Glide.with(avatarInset) - .load(R.drawable.ic_bot_24dp) - .into(avatarInset); + Glide.with(avatarInset).load(R.drawable.ic_bot_24dp).into(avatarInset); } else { avatarInset.setVisibility(View.GONE); @@ -316,18 +353,21 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { avatarInset.setVisibility(View.VISIBLE); avatarInset.setBackground(null); - ImageLoadingHelper.loadAvatar(rebloggedUrl, avatarInset, avatarRadius24dp, + ImageLoadingHelper.loadAvatar( + rebloggedUrl, + avatarInset, + avatarRadius24dp, statusDisplayOptions.animateAvatars()); avatarRadius = avatarRadius36dp; } - ImageLoadingHelper.loadAvatar(url, avatar, avatarRadius, - statusDisplayOptions.animateAvatars()); - + ImageLoadingHelper.loadAvatar( + url, avatar, avatarRadius, statusDisplayOptions.animateAvatars()); } - protected void setCreatedAt(Date createdAt, Date editedAt, StatusDisplayOptions statusDisplayOptions) { + protected void setCreatedAt( + Date createdAt, Date editedAt, StatusDisplayOptions statusDisplayOptions) { String timestampText; if (statusDisplayOptions.useAbsoluteTime()) { timestampText = getAbsoluteTime(createdAt); @@ -337,14 +377,19 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { } else { long then = createdAt.getTime(); long now = System.currentTimeMillis(); - String readout = TimestampUtils.getRelativeTimeSpanString(timestampInfo.getContext(), then, now); + String readout = + TimestampUtils.getRelativeTimeSpanString( + timestampInfo.getContext(), then, now); timestampText = readout; } } if (editedAt != null) { - timestampText = timestampInfo.getContext() - .getString(R.string.status_timestamp_with_edited_indicator, timestampText); + timestampText = + timestampInfo + .getContext() + .getString( + R.string.status_timestamp_with_edited_indicator, timestampText); } timestampInfo.setText(timestampText); } @@ -360,8 +405,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { } } - private CharSequence getCreatedAtDescription(Date createdAt, - StatusDisplayOptions statusDisplayOptions) { + private CharSequence getCreatedAtDescription( + Date createdAt, StatusDisplayOptions statusDisplayOptions) { if (statusDisplayOptions.useAbsoluteTime()) { return getAbsoluteTime(createdAt); } else { @@ -373,9 +418,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { } else { long then = createdAt.getTime(); long now = System.currentTimeMillis(); - return DateUtils.getRelativeTimeSpanString(then, now, - DateUtils.SECOND_IN_MILLIS, - DateUtils.FORMAT_ABBREV_RELATIVE); + return DateUtils.getRelativeTimeSpanString( + then, now, DateUtils.SECOND_IN_MILLIS, DateUtils.FORMAT_ABBREV_RELATIVE); } } } @@ -386,7 +430,6 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { } else { replyButton.setImageResource(R.drawable.ic_reply_24dp); } - } protected void setReplyInfo(StatusViewData.Concrete status, StatusActionListener listener) { @@ -451,20 +494,18 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { return ImageLoadingHelper.decodeBlurHash(this.avatar.getContext(), blurhash); } - private void loadImage(MediaPreviewImageView imageView, - @Nullable String previewUrl, - @Nullable MetaData meta, - @Nullable String blurhash) { + private void loadImage( + MediaPreviewImageView imageView, + @Nullable String previewUrl, + @Nullable MetaData meta, + @Nullable String blurhash) { Drawable placeholder = blurhash != null ? decodeBlurHash(blurhash) : mediaPreviewUnloaded; if (TextUtils.isEmpty(previewUrl)) { imageView.removeFocalPoint(); - Glide.with(imageView) - .load(placeholder) - .centerInside() - .into(imageView); + Glide.with(imageView).load(placeholder).centerInside().into(imageView); } else { Focus focus = meta != null ? meta.getFocus() : null; @@ -490,13 +531,15 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { } } - protected void setMediaPreviews(final List<Attachment> attachments, boolean sensitive, - final StatusActionListener listener, boolean showingContent, - boolean useBlurhash) { + protected void setMediaPreviews( + final List<Attachment> attachments, + boolean sensitive, + final StatusActionListener listener, + boolean showingContent, + boolean useBlurhash) { Context context = itemView.getContext(); final int n = Math.min(attachments.size(), Status.MAX_MEDIA_ATTACHMENTS); - final int mediaPreviewHeight = getMediaPreviewHeight(context); if (n <= 2) { @@ -518,8 +561,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { imageView.setVisibility(View.VISIBLE); if (TextUtils.isEmpty(description)) { - imageView.setContentDescription(imageView.getContext() - .getString(R.string.action_view_media)); + imageView.setContentDescription( + imageView.getContext().getString(R.string.action_view_media)); } else { imageView.setContentDescription(description); } @@ -528,8 +571,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { imageView, showingContent ? previewUrl : null, attachment.getMeta(), - useBlurhash ? attachment.getBlurhash() : null - ); + useBlurhash ? attachment.getBlurhash() : null); final Attachment.Type type = attachment.getType(); if (showingContent && (type == Attachment.Type.VIDEO || type == Attachment.Type.GIFV)) { @@ -549,21 +591,22 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { sensitiveMediaWarning.setVisibility(showingContent ? View.GONE : View.VISIBLE); sensitiveMediaShow.setVisibility(showingContent ? View.VISIBLE : View.GONE); - sensitiveMediaShow.setOnClickListener(v -> { - if (getAdapterPosition() != RecyclerView.NO_POSITION) { - listener.onContentHiddenChange(false, getAdapterPosition()); - } - v.setVisibility(View.GONE); - sensitiveMediaWarning.setVisibility(View.VISIBLE); - }); - sensitiveMediaWarning.setOnClickListener(v -> { - if (getAdapterPosition() != RecyclerView.NO_POSITION) { - listener.onContentHiddenChange(true, getAdapterPosition()); - } - v.setVisibility(View.GONE); - sensitiveMediaShow.setVisibility(View.VISIBLE); - }); - + sensitiveMediaShow.setOnClickListener( + v -> { + if (getAdapterPosition() != RecyclerView.NO_POSITION) { + listener.onContentHiddenChange(false, getAdapterPosition()); + } + v.setVisibility(View.GONE); + sensitiveMediaWarning.setVisibility(View.VISIBLE); + }); + sensitiveMediaWarning.setOnClickListener( + v -> { + if (getAdapterPosition() != RecyclerView.NO_POSITION) { + listener.onContentHiddenChange(true, getAdapterPosition()); + } + v.setVisibility(View.GONE); + sensitiveMediaShow.setVisibility(View.VISIBLE); + }); // Hide any of the placeholder previews beyond the ones set. for (int i = n; i < Status.MAX_MEDIA_ATTACHMENTS; i++) { @@ -588,14 +631,18 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { private void updateMediaLabel(int index, boolean sensitive, boolean showingContent) { Context context = itemView.getContext(); - CharSequence label = (sensitive && !showingContent) ? - context.getString(R.string.status_sensitive_media_title) : - mediaDescriptions[index]; + CharSequence label = + (sensitive && !showingContent) + ? context.getString(R.string.status_sensitive_media_title) + : mediaDescriptions[index]; mediaLabels[index].setText(label); } - protected void setMediaLabel(List<Attachment> attachments, boolean sensitive, - final StatusActionListener listener, boolean showingContent) { + protected void setMediaLabel( + List<Attachment> attachments, + boolean sensitive, + final StatusActionListener listener, + boolean showingContent) { Context context = itemView.getContext(); for (int i = 0; i < mediaLabels.length; i++) { TextView mediaLabel = mediaLabels[i]; @@ -616,32 +663,43 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { } } - private void setAttachmentClickListener(View view, StatusActionListener listener, - int index, Attachment attachment, boolean animateTransition) { - view.setOnClickListener(v -> { - int position = getAdapterPosition(); - if (position != RecyclerView.NO_POSITION) { - if (sensitiveMediaWarning.getVisibility() == View.VISIBLE) { - listener.onContentHiddenChange(true, getAdapterPosition()); - } else { - listener.onViewMedia(position, index, animateTransition ? v : null); - } - } - }); - view.setOnLongClickListener(v -> { - CharSequence description = getAttachmentDescription(view.getContext(), attachment); - Toast.makeText(view.getContext(), description, Toast.LENGTH_LONG).show(); - return true; - }); + private void setAttachmentClickListener( + View view, + StatusActionListener listener, + int index, + Attachment attachment, + boolean animateTransition) { + view.setOnClickListener( + v -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + if (sensitiveMediaWarning.getVisibility() == View.VISIBLE) { + listener.onContentHiddenChange(true, getAdapterPosition()); + } else { + listener.onViewMedia(position, index, animateTransition ? v : null); + } + } + }); + view.setOnLongClickListener( + v -> { + CharSequence description = + getAttachmentDescription(view.getContext(), attachment); + Toast.makeText(view.getContext(), description, Toast.LENGTH_LONG).show(); + return true; + }); } private static CharSequence getAttachmentDescription(Context context, Attachment attachment) { String duration = ""; - if (attachment.getMeta() != null && attachment.getMeta().getDuration() != null && attachment.getMeta().getDuration() > 0) { + if (attachment.getMeta() != null + && attachment.getMeta().getDuration() != null + && attachment.getMeta().getDuration() > 0) { duration = formatDuration(attachment.getMeta().getDuration()) + " "; } if (TextUtils.isEmpty(attachment.getDescription())) { - return duration + context.getString(R.string.description_status_media_no_description_placeholder); + return duration + + context.getString( + R.string.description_status_media_no_description_placeholder); } else { return duration + attachment.getDescription(); } @@ -652,122 +710,144 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { sensitiveMediaShow.setVisibility(View.GONE); } - protected void setupButtons(final StatusActionListener listener, - final String accountId, - final String statusContent, - StatusDisplayOptions statusDisplayOptions) { - View.OnClickListener profileButtonClickListener = button -> { - listener.onViewAccount(accountId); - }; + protected void setupButtons( + final StatusActionListener listener, + final String accountId, + final String statusContent, + StatusDisplayOptions statusDisplayOptions) { + View.OnClickListener profileButtonClickListener = + button -> { + listener.onViewAccount(accountId); + }; avatar.setOnClickListener(profileButtonClickListener); displayName.setOnClickListener(profileButtonClickListener); - replyButton.setOnClickListener(v -> { - int position = getAdapterPosition(); - if (position != RecyclerView.NO_POSITION) { - listener.onReply(position); - } - }); - if (reblogButton != null) { - reblogButton.setEventListener((button, buttonState) -> { - // return true to play animaion - int position = getAdapterPosition(); - if (position != RecyclerView.NO_POSITION) { - if (statusDisplayOptions.confirmReblogs()) { - showConfirmReblogDialog(listener, statusContent, buttonState, position); - return false; - } else { - listener.onReblog(!buttonState, position); - return true; + replyButton.setOnClickListener( + v -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onReply(position); } - } else { - return false; - } - }); + }); + if (reblogButton != null) { + reblogButton.setEventListener( + (button, buttonState) -> { + // return true to play animaion + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + if (statusDisplayOptions.confirmReblogs()) { + showConfirmReblogDialog( + listener, statusContent, buttonState, position); + return false; + } else { + listener.onReblog(!buttonState, position); + return true; + } + } else { + return false; + } + }); } - favouriteButton.setEventListener((button, buttonState) -> { - int position = getAdapterPosition(); - if (position != RecyclerView.NO_POSITION) { - listener.onFavourite(!buttonState, position); - } - return true; - }); + favouriteButton.setEventListener( + (button, buttonState) -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onFavourite(!buttonState, position); + } + return true; + }); - bookmarkButton.setEventListener((button, buttonState) -> { - int position = getAdapterPosition(); - if (position != RecyclerView.NO_POSITION) { - listener.onBookmark(!buttonState, position); - } - return true; - }); + bookmarkButton.setEventListener( + (button, buttonState) -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onBookmark(!buttonState, position); + } + return true; + }); - moreButton.setOnClickListener(v -> { - int position = getAdapterPosition(); - if (position != RecyclerView.NO_POSITION) { - listener.onMore(v, position); - } - }); + moreButton.setOnClickListener( + v -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onMore(v, position); + } + }); /* Even though the content TextView is a child of the container, it won't respond to clicks * if it contains URLSpans without also setting its listener. The surrounding spans will * just eat the clicks instead of deferring to the parent listener, but WILL respond to a * listener directly on the TextView, for whatever reason. */ - View.OnClickListener viewThreadListener = v -> { - int position = getAdapterPosition(); - if (position != RecyclerView.NO_POSITION) { - listener.onViewThread(position); - } - }; + View.OnClickListener viewThreadListener = + v -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onViewThread(position); + } + }; content.setOnClickListener(viewThreadListener); itemView.setOnClickListener(viewThreadListener); } - private void showConfirmReblogDialog(StatusActionListener listener, - String statusContent, - boolean buttonState, - int position) { + private void showConfirmReblogDialog( + StatusActionListener listener, + String statusContent, + boolean buttonState, + int position) { int okButtonTextId = buttonState ? R.string.action_unreblog : R.string.action_reblog; new AlertDialog.Builder(reblogButton.getContext()) .setMessage(statusContent) - .setPositiveButton(okButtonTextId, (__, ___) -> { - listener.onReblog(!buttonState, position); - if (!buttonState) { - // Play animation only when it's reblog, not unreblog - reblogButton.playAnimation(); - } - }) + .setPositiveButton( + okButtonTextId, + (__, ___) -> { + listener.onReblog(!buttonState, position); + if (!buttonState) { + // Play animation only when it's reblog, not unreblog + reblogButton.playAnimation(); + } + }) .show(); } - private void setEmojiReactions(@Nullable List<EmojiReaction> reactions, final StatusActionListener listener, final String statusId) { - if(reactButton != null) { - reactButton.setOnClickListener(v -> { - EmojiKeyboard.show(reactButton.getContext(), statusId, EmojiKeyboard.UNICODE_MODE, (id, emoji) -> { - listener.onEmojiReact(true, emoji, id); - }); - }); + private void setEmojiReactions( + @Nullable List<EmojiReaction> reactions, + final StatusActionListener listener, + final String statusId) { + if (reactButton != null) { + reactButton.setOnClickListener( + v -> { + EmojiKeyboard.show( + reactButton.getContext(), + statusId, + EmojiKeyboard.UNICODE_MODE, + (id, emoji) -> { + listener.onEmojiReact(true, emoji, id); + }); + }); } - if(emojiReactionsView != null ) { - if(reactions != null && reactions.size() > 0) { + if (emojiReactionsView != null) { + if (reactions != null && reactions.size() > 0) { emojiReactionsView.setVisibility(View.VISIBLE); FlexboxLayoutManager lm = new FlexboxLayoutManager(emojiReactionsView.getContext()); emojiReactionsView.setLayoutManager(lm); - emojiReactionsView.setAdapter(new EmojiReactionsAdapter(reactions, listener, statusId)); - emojiReactionsView.setOnTouchListener(new View.OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - if(event.getAction() == MotionEvent.ACTION_POINTER_UP || - event.getAction() == MotionEvent.ACTION_UP) { - int position = getAdapterPosition(); - if(position != RecyclerView.NO_POSITION) - listener.onViewThread(position); - } - return false; - } - }); + emojiReactionsView.setAdapter( + new EmojiReactionsAdapter(reactions, listener, statusId)); + emojiReactionsView.setOnTouchListener( + new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_POINTER_UP + || event.getAction() == MotionEvent.ACTION_UP) { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) + listener.onViewThread(position); + } + return false; + } + }); } else { emojiReactionsView.setVisibility(View.GONE); emojiReactionsView.setLayoutManager(null); @@ -776,29 +856,42 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { } } - public void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener, - StatusDisplayOptions statusDisplayOptions) { + public void setupWithStatus( + StatusViewData.Concrete status, + final StatusActionListener listener, + StatusDisplayOptions statusDisplayOptions) { this.setupWithStatus(status, listener, statusDisplayOptions, null); } - protected void setupWithStatus(StatusViewData.Concrete status, - final StatusActionListener listener, - StatusDisplayOptions statusDisplayOptions, - @Nullable Object payloads) { + protected void setupWithStatus( + StatusViewData.Concrete status, + final StatusActionListener listener, + StatusDisplayOptions statusDisplayOptions, + @Nullable Object payloads) { if (payloads == null) { setDisplayName(status.getUserFullName(), status.getAccountEmojis()); setUsername(status.getNickname()); setCreatedAt(status.getCreatedAt(), status.getEditedAt(), statusDisplayOptions); setIsReply(status.getInReplyToId() != null); setReplyInfo(status, listener); - setAvatar(status.getAvatar(), status.getRebloggedAvatar(), status.isBot(), statusDisplayOptions); + setAvatar( + status.getAvatar(), + status.getRebloggedAvatar(), + status.isBot(), + statusDisplayOptions); setReblogged(status.isReblogged()); setFavourited(status.isFavourited()); setBookmarked(status.isBookmarked()); List<Attachment> attachments = status.getAttachments(); boolean sensitive = status.isSensitive(); - if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) { - setMediaPreviews(attachments, sensitive, listener, status.isShowingContent(), statusDisplayOptions.useBlurhash()); + if (statusDisplayOptions.mediaPreviewEnabled() + && hasPreviewableAttachment(attachments)) { + setMediaPreviews( + attachments, + sensitive, + listener, + status.isShowingContent(), + statusDisplayOptions.useBlurhash()); if (attachments.size() == 0) { hideSensitiveMediaWarning(); @@ -821,11 +914,22 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { setupCard(status, statusDisplayOptions.cardViewMode(), statusDisplayOptions); } - setupButtons(listener, status.getSenderId(), status.getContent().toString(), + setupButtons( + listener, + status.getSenderId(), + status.getContent().toString(), statusDisplayOptions); setRebloggingEnabled(status.getRebloggingEnabled(), status.getVisibility()); - setSpoilerAndContent(status.isExpanded(), status.getContent(), status.getSpoilerText(), status.getMentions(), status.getStatusEmojis(), status.getPoll(), statusDisplayOptions, listener); + setSpoilerAndContent( + status.isExpanded(), + status.getContent(), + status.getSpoilerText(), + status.getMentions(), + status.getStatusEmojis(), + status.getPoll(), + statusDisplayOptions, + listener); setDescriptionForStatus(status, statusDisplayOptions); @@ -841,80 +945,96 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { if (payloads instanceof List) for (Object item : (List<?>) payloads) { if (Key.KEY_CREATED.equals(item)) { - setCreatedAt(status.getCreatedAt(), status.getEditedAt(), statusDisplayOptions); + setCreatedAt( + status.getCreatedAt(), status.getEditedAt(), statusDisplayOptions); } } - } } protected static boolean hasPreviewableAttachment(List<Attachment> attachments) { for (Attachment attachment : attachments) { - if (attachment.getType() == Attachment.Type.AUDIO || attachment.getType() == Attachment.Type.UNKNOWN) { + if (attachment.getType() == Attachment.Type.AUDIO + || attachment.getType() == Attachment.Type.UNKNOWN) { return false; } } return true; } - private void setDescriptionForStatus(@NonNull StatusViewData.Concrete status, - StatusDisplayOptions statusDisplayOptions) { + private void setDescriptionForStatus( + @NonNull StatusViewData.Concrete status, StatusDisplayOptions statusDisplayOptions) { Context context = itemView.getContext(); - String description = context.getString(R.string.description_status, - status.getUserFullName(), - getContentWarningDescription(context, status), - (TextUtils.isEmpty(status.getSpoilerText()) || !status.isSensitive() || status.isExpanded() ? status.getContent() : ""), - getCreatedAtDescription(status.getCreatedAt(), statusDisplayOptions), - status.getEditedAt() != null ? context.getString(R.string.description_status_edited) : "", - getReblogDescription(context, status), - status.getNickname(), - status.isReblogged() ? context.getString(R.string.description_status_reblogged) : "", - status.isFavourited() ? context.getString(R.string.description_status_favourited) : "", - status.isBookmarked() ? context.getString(R.string.description_status_bookmarked) : "", - getMediaDescription(context, status), - getVisibilityDescription(context, status.getVisibility()), - getFavsText(context, status.getFavouritesCount()), - getReblogsText(context, status.getReblogsCount()), - getPollDescription(status, context, statusDisplayOptions) - ); + String description = + context.getString( + R.string.description_status, + status.getUserFullName(), + getContentWarningDescription(context, status), + (TextUtils.isEmpty(status.getSpoilerText()) + || !status.isSensitive() + || status.isExpanded() + ? status.getContent() + : ""), + getCreatedAtDescription(status.getCreatedAt(), statusDisplayOptions), + status.getEditedAt() != null + ? context.getString(R.string.description_status_edited) + : "", + getReblogDescription(context, status), + status.getNickname(), + status.isReblogged() + ? context.getString(R.string.description_status_reblogged) + : "", + status.isFavourited() + ? context.getString(R.string.description_status_favourited) + : "", + status.isBookmarked() + ? context.getString(R.string.description_status_bookmarked) + : "", + getMediaDescription(context, status), + getVisibilityDescription(context, status.getVisibility()), + getFavsText(context, status.getFavouritesCount()), + getReblogsText(context, status.getReblogsCount()), + getPollDescription(status, context, statusDisplayOptions)); itemView.setContentDescription(description); } - private static CharSequence getReblogDescription(Context context, - @NonNull StatusViewData.Concrete status) { + private static CharSequence getReblogDescription( + Context context, @NonNull StatusViewData.Concrete status) { String rebloggedUsername = status.getRebloggedByUsername(); if (rebloggedUsername != null) { - return context - .getString(R.string.status_boosted_format, rebloggedUsername); + return context.getString(R.string.status_boosted_format, rebloggedUsername); } else { return ""; } } - private static CharSequence getMediaDescription(Context context, - @NonNull StatusViewData.Concrete status) { + private static CharSequence getMediaDescription( + Context context, @NonNull StatusViewData.Concrete status) { if (status.getAttachments().isEmpty()) { return ""; } - StringBuilder mediaDescriptions = CollectionsKt.fold( - status.getAttachments(), - new StringBuilder(), - (builder, a) -> { - if (a.getDescription() == null) { - String placeholder = - context.getString(R.string.description_status_media_no_description_placeholder); - return builder.append(placeholder); - } else { - builder.append("; "); - return builder.append(a.getDescription()); - } - }); + StringBuilder mediaDescriptions = + CollectionsKt.fold( + status.getAttachments(), + new StringBuilder(), + (builder, a) -> { + if (a.getDescription() == null) { + String placeholder = + context.getString( + R.string + .description_status_media_no_description_placeholder); + return builder.append(placeholder); + } else { + builder.append("; "); + return builder.append(a.getDescription()); + } + }); return context.getString(R.string.description_status_media, mediaDescriptions); } - private static CharSequence getContentWarningDescription(Context context, - @NonNull StatusViewData.Concrete status) { + private static CharSequence getContentWarningDescription( + Context context, @NonNull StatusViewData.Concrete status) { if (!TextUtils.isEmpty(status.getSpoilerText())) { return context.getString(R.string.description_status_cw, status.getSpoilerText()); } else { @@ -922,7 +1042,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { } } - private static CharSequence getVisibilityDescription(Context context, Status.Visibility visibility) { + private static CharSequence getVisibilityDescription( + Context context, Status.Visibility visibility) { if (visibility == null) { return ""; @@ -951,9 +1072,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { return context.getString(resource); } - private CharSequence getPollDescription(@NonNull StatusViewData.Concrete status, - Context context, - StatusDisplayOptions statusDisplayOptions) { + private CharSequence getPollDescription( + @NonNull StatusViewData.Concrete status, + Context context, + StatusDisplayOptions statusDisplayOptions) { PollViewData poll = status.getPoll(); if (poll == null) { return ""; @@ -962,14 +1084,17 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { List<PollOptionViewData> options = poll.getOptions(); for (int i = 0; i < args.length; i++) { if (i < options.size()) { - int percent = PollViewDataKt.calculatePercent(options.get(i).getVotesCount(), poll.getVotesCount()); + int percent = + PollViewDataKt.calculatePercent( + options.get(i).getVotesCount(), poll.getVotesCount()); args[i] = buildDescription(options.get(i).getTitle(), percent, context); } else { args[i] = ""; } } - args[4] = getPollInfoText(System.currentTimeMillis(), poll, statusDisplayOptions, - context); + args[4] = + getPollInfoText( + System.currentTimeMillis(), poll, statusDisplayOptions, context); return context.getString(R.string.description_poll, args); } } @@ -977,7 +1102,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { protected CharSequence getFavsText(Context context, int count) { if (count > 0) { String countString = numberFormat.format(count); - return HtmlCompat.fromHtml(context.getResources().getQuantityString(R.plurals.favs, count, countString), HtmlCompat.FROM_HTML_MODE_LEGACY); + return HtmlCompat.fromHtml( + context.getResources().getQuantityString(R.plurals.favs, count, countString), + HtmlCompat.FROM_HTML_MODE_LEGACY); } else { return ""; } @@ -986,18 +1113,25 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { protected CharSequence getReblogsText(Context context, int count) { if (count > 0) { String countString = numberFormat.format(count); - return HtmlCompat.fromHtml(context.getResources().getQuantityString(R.plurals.reblogs, count, countString), HtmlCompat.FROM_HTML_MODE_LEGACY); + return HtmlCompat.fromHtml( + context.getResources().getQuantityString(R.plurals.reblogs, count, countString), + HtmlCompat.FROM_HTML_MODE_LEGACY); } else { return ""; } } - private void setupPoll(PollViewData poll, List<Emoji> emojis, - StatusDisplayOptions statusDisplayOptions, - StatusActionListener listener) { + private void setupPoll( + PollViewData poll, + List<Emoji> emojis, + StatusDisplayOptions statusDisplayOptions, + StatusActionListener listener) { long timestamp = System.currentTimeMillis(); - boolean expired = poll.getExpired() || (poll.getExpiresAt() != null && timestamp > poll.getExpiresAt().getTime()); + boolean expired = + poll.getExpired() + || (poll.getExpiresAt() != null + && timestamp > poll.getExpiresAt().getTime()); Context context = pollDescription.getContext(); @@ -1005,53 +1139,73 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { if (expired || poll.getVoted()) { // no voting possible - 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); + 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, null); + pollAdapter.setup( + poll.getOptions(), + poll.getVotesCount(), + poll.getVotersCount(), + emojis, + poll.getMultiple() ? PollAdapter.MULTIPLE : PollAdapter.SINGLE, + null); pollButton.setVisibility(View.VISIBLE); - pollButton.setOnClickListener(v -> { - - int position = getAdapterPosition(); + pollButton.setOnClickListener( + v -> { + int position = getAdapterPosition(); - if (position != RecyclerView.NO_POSITION) { + if (position != RecyclerView.NO_POSITION) { - List<Integer> pollResult = pollAdapter.getSelected(); - - if (!pollResult.isEmpty()) { - listener.onVoteInPoll(position, pollResult); - } - } + List<Integer> pollResult = pollAdapter.getSelected(); - }); + if (!pollResult.isEmpty()) { + listener.onVoteInPoll(position, pollResult); + } + } + }); } pollDescription.setVisibility(View.VISIBLE); pollDescription.setText(getPollInfoText(timestamp, poll, statusDisplayOptions, context)); } - private CharSequence getPollInfoText(long timestamp, PollViewData poll, - StatusDisplayOptions statusDisplayOptions, - Context context) { + private CharSequence getPollInfoText( + long timestamp, + PollViewData poll, + StatusDisplayOptions statusDisplayOptions, + Context context) { String votesText; - if(poll.getVotersCount() == null) { + if (poll.getVotersCount() == null) { String voters = numberFormat.format(poll.getVotesCount()); - votesText = context.getResources().getQuantityString(R.plurals.poll_info_votes, poll.getVotesCount(), voters); + votesText = + context.getResources() + .getQuantityString( + R.plurals.poll_info_votes, poll.getVotesCount(), voters); } else { // TODO: Change back to getVotersCount when this class is migrated to Kotlin String voters = numberFormat.format(poll.getVotesCount()); - votesText = context.getResources().getQuantityString(R.plurals.poll_info_people, poll.getVotesCount(), voters); + votesText = + context.getResources() + .getQuantityString( + R.plurals.poll_info_people, poll.getVotesCount(), voters); } CharSequence pollDurationInfo; if (poll.getExpired()) { @@ -1060,25 +1214,38 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { return votesText; } else { if (statusDisplayOptions.useAbsoluteTime()) { - pollDurationInfo = context.getString(R.string.poll_info_time_absolute, getAbsoluteTime(poll.getExpiresAt())); + pollDurationInfo = + context.getString( + R.string.poll_info_time_absolute, + getAbsoluteTime(poll.getExpiresAt())); } else { - pollDurationInfo = TimestampUtils.formatPollDuration(pollDescription.getContext(), poll.getExpiresAt().getTime(), timestamp); + pollDurationInfo = + TimestampUtils.formatPollDuration( + pollDescription.getContext(), + poll.getExpiresAt().getTime(), + timestamp); } } - return pollDescription.getContext().getString(R.string.poll_info_format, votesText, pollDurationInfo); + return pollDescription + .getContext() + .getString(R.string.poll_info_format, votesText, pollDurationInfo); } - protected void setupCard(StatusViewData.Concrete status, CardViewMode cardViewMode, StatusDisplayOptions statusDisplayOptions) { - if (cardViewMode != CardViewMode.NONE && - status.getAttachments().size() == 0 && - status.getCard() != null && - !TextUtils.isEmpty(status.getCard().getUrl()) && - (!status.isCollapsible() || !status.isCollapsed())) { + protected void setupCard( + StatusViewData.Concrete status, + CardViewMode cardViewMode, + StatusDisplayOptions statusDisplayOptions) { + if (cardViewMode != CardViewMode.NONE + && status.getAttachments().size() == 0 + && status.getCard() != null + && !TextUtils.isEmpty(status.getCard().getUrl()) + && (!status.isCollapsible() || !status.isCollapsed())) { final Card card = status.getCard(); cardView.setVisibility(View.VISIBLE); cardTitle.setText(card.getTitle()); - if (TextUtils.isEmpty(card.getDescription()) && TextUtils.isEmpty(card.getAuthorName())) { + if (TextUtils.isEmpty(card.getDescription()) + && TextUtils.isEmpty(card.getAuthorName())) { cardDescription.setVisibility(View.GONE); } else { cardDescription.setVisibility(View.VISIBLE); @@ -1091,24 +1258,33 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { cardUrl.setText(card.getUrl()); - // Statuses from other activitypub sources can be marked sensitive even if there's no media, + // 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())) { + if (statusDisplayOptions.mediaPreviewEnabled() + && !status.isSensitive() + && !TextUtils.isEmpty(card.getImage())) { int topLeftRadius = 0; int topRightRadius = 0; int bottomRightRadius = 0; int bottomLeftRadius = 0; - int radius = cardImage.getContext().getResources() - .getDimensionPixelSize(R.dimen.card_radius); + int radius = + cardImage + .getContext() + .getResources() + .getDimensionPixelSize(R.dimen.card_radius); if (card.getWidth() > card.getHeight()) { cardView.setOrientation(LinearLayout.VERTICAL); - cardImage.getLayoutParams().height = cardImage.getContext().getResources() - .getDimensionPixelSize(R.dimen.card_image_vertical_height); + cardImage.getLayoutParams().height = + cardImage + .getContext() + .getResources() + .getDimensionPixelSize(R.dimen.card_image_vertical_height); cardImage.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT; cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT; cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT; @@ -1117,8 +1293,11 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { } else { cardView.setOrientation(LinearLayout.HORIZONTAL); cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT; - cardImage.getLayoutParams().width = cardImage.getContext().getResources() - .getDimensionPixelSize(R.dimen.card_image_horizontal_width); + 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; topLeftRadius = radius; @@ -1130,31 +1309,43 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { builder = builder.placeholder(decodeBlurHash(card.getBlurhash())); } builder.transform( - new CenterCrop(), - new GranularRoundedCorners(topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius) - ) + 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); + } else if (statusDisplayOptions.useBlurhash() + && !TextUtils.isEmpty(card.getBlurhash())) { + int radius = + cardImage + .getContext() + .getResources() + .getDimensionPixelSize(R.dimen.card_radius); cardView.setOrientation(LinearLayout.HORIZONTAL); cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT; - cardImage.getLayoutParams().width = cardImage.getContext().getResources() - .getDimensionPixelSize(R.dimen.card_image_horizontal_width); + 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())) + Glide.with(cardImage) + .load(decodeBlurHash(card.getBlurhash())) .transform( - new CenterCrop(), - new GranularRoundedCorners(radius, 0, 0, radius) - ) + new CenterCrop(), new GranularRoundedCorners(radius, 0, 0, radius)) .into(cardImage); } else { cardView.setOrientation(LinearLayout.HORIZONTAL); cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT; - cardImage.getLayoutParams().width = cardImage.getContext().getResources() - .getDimensionPixelSize(R.dimen.card_image_horizontal_width); + 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; cardImage.setImageResource(R.drawable.card_image_placeholder); @@ -1174,5 +1365,4 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { return String.format("%d:%02d:%02d", hours, minutes, seconds); } - } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java index 5fce8d7..33ff991 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java @@ -6,30 +6,22 @@ import android.content.Context; import android.graphics.drawable.Drawable; import android.text.TextUtils; import android.text.method.LinkMovementMethod; -import android.view.LayoutInflater; import android.view.View; import android.widget.TextView; import android.widget.Toast; -import android.util.Log; - import androidx.annotation.Nullable; -import androidx.emoji.widget.EmojiAppCompatButton; import androidx.recyclerview.widget.RecyclerView; -import com.google.android.flexbox.FlexboxLayoutManager; - import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.entity.Status; -import com.keylesspalace.tusky.entity.EmojiReaction; import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.util.CardViewMode; import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.viewdata.StatusViewData; - import java.text.DateFormat; import java.util.ArrayList; -import java.util.List; import java.util.Date; +import java.util.List; class StatusDetailedViewHolder extends StatusBaseViewHolder { private TextView reblogs; @@ -45,22 +37,25 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder { @Override protected int getMediaPreviewHeight(Context context) { - return context.getResources().getDimensionPixelSize(R.dimen.status_detail_media_preview_height); + return context.getResources() + .getDimensionPixelSize(R.dimen.status_detail_media_preview_height); } @Override - protected void setCreatedAt(Date createdAt, Date editedAt, StatusDisplayOptions statusDisplayOptions) { - DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.SHORT); + protected void setCreatedAt( + Date createdAt, Date editedAt, StatusDisplayOptions statusDisplayOptions) { + DateFormat dateFormat = + DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.SHORT); Context context = timestampInfo.getContext(); List<String> list = new ArrayList<>(); - if (createdAt != null) - list.add(dateFormat.format(createdAt)); + if (createdAt != null) list.add(dateFormat.format(createdAt)); if (editedAt != null) list.add(context.getString(R.string.status_edited, dateFormat.format(editedAt))); timestampInfo.setText(TextUtils.join(context.getString(R.string.timestamp_joiner), list)); } - private void setReblogAndFavCount(int reblogCount, int favCount, StatusActionListener listener) { + private void setReblogAndFavCount( + int reblogCount, int favCount, StatusActionListener listener) { if (reblogCount > 0) { reblogs.setText(getReblogsText(reblogs.getContext(), reblogCount)); @@ -81,18 +76,20 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder { infoDivider.setVisibility(View.VISIBLE); } - reblogs.setOnClickListener(v -> { - int position = getAdapterPosition(); - if (position != RecyclerView.NO_POSITION) { - listener.onShowReblogs(position); - } - }); - favourites.setOnClickListener(v -> { - int position = getAdapterPosition(); - if (position != RecyclerView.NO_POSITION) { - listener.onShowFavs(position); - } - }); + reblogs.setOnClickListener( + v -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onShowReblogs(position); + } + }); + favourites.setOnClickListener( + v -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onShowFavs(position); + } + }); } private void setApplication(@Nullable Status.Application app) { @@ -109,33 +106,46 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder { } } } - + @Override - protected void setupWithStatus(final StatusViewData.Concrete status, - final StatusActionListener listener, - StatusDisplayOptions statusDisplayOptions, - @Nullable Object payloads) { + protected void setupWithStatus( + final StatusViewData.Concrete status, + final StatusActionListener listener, + StatusDisplayOptions statusDisplayOptions, + @Nullable Object payloads) { super.setupWithStatus(status, listener, statusDisplayOptions, payloads); - setupCard(status, CardViewMode.FULL_WIDTH, statusDisplayOptions); // Always show card for detailed status + setupCard( + status, + CardViewMode.FULL_WIDTH, + statusDisplayOptions); // Always show card for detailed status if (payloads == null) { if (!statusDisplayOptions.hideStats()) { - setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener); + setReblogAndFavCount( + status.getReblogsCount(), status.getFavouritesCount(), listener); } else { hideQuantitativeStats(); } setApplication(status.getApplication()); - View.OnLongClickListener longClickListener = view -> { - TextView textView = (TextView) view; - ClipboardManager clipboard = (ClipboardManager) view.getContext().getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText("toot", textView.getText()); - clipboard.setPrimaryClip(clip); - - Toast.makeText(view.getContext(), R.string.copy_to_clipboard_success, Toast.LENGTH_SHORT).show(); - - return true; - }; + View.OnLongClickListener longClickListener = + view -> { + TextView textView = (TextView) view; + ClipboardManager clipboard = + (ClipboardManager) + view.getContext() + .getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("toot", textView.getText()); + clipboard.setPrimaryClip(clip); + + Toast.makeText( + view.getContext(), + R.string.copy_to_clipboard_success, + Toast.LENGTH_SHORT) + .show(); + + return true; + }; content.setOnLongClickListener(longClickListener); contentWarningDescription.setOnLongClickListener(longClickListener); @@ -170,26 +180,16 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder { return; } - final Drawable visibilityDrawable = this.timestampInfo.getContext() - .getDrawable(visibilityIcon); + final Drawable visibilityDrawable = + this.timestampInfo.getContext().getDrawable(visibilityIcon); if (visibilityDrawable == null) { return; } final int size = (int) this.timestampInfo.getTextSize(); - visibilityDrawable.setBounds( - 0, - 0, - size, - size - ); + visibilityDrawable.setBounds(0, 0, size, size); visibilityDrawable.setTint(this.timestampInfo.getCurrentTextColor()); - this.timestampInfo.setCompoundDrawables( - visibilityDrawable, - null, - null, - null - ); + this.timestampInfo.setCompoundDrawables(visibilityDrawable, null, null, null); } private void hideQuantitativeStats() { diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java index 4043f90..977a19e 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java @@ -20,12 +20,11 @@ import android.text.InputFilter; import android.text.TextUtils; import android.view.View; import android.widget.Button; -import android.widget.TextView; import android.widget.ImageButton; - +import android.widget.TextView; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; - +import at.connyduck.sparkbutton.helpers.Utils; import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.util.CustomEmojiHelper; @@ -34,10 +33,9 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.util.StringUtils; import com.keylesspalace.tusky.viewdata.StatusViewData; -import at.connyduck.sparkbutton.helpers.Utils; - public class StatusViewHolder extends StatusBaseViewHolder { - private static final InputFilter[] COLLAPSE_INPUT_FILTER = new InputFilter[]{SmartLengthInputFilter.INSTANCE}; + private static final InputFilter[] COLLAPSE_INPUT_FILTER = + new InputFilter[] {SmartLengthInputFilter.INSTANCE}; private static final InputFilter[] NO_INPUT_FILTER = new InputFilter[0]; private TextView statusInfo; @@ -48,7 +46,7 @@ public class StatusViewHolder extends StatusBaseViewHolder { super(itemView); statusInfo = itemView.findViewById(R.id.status_info); contentCollapseButton = itemView.findViewById(R.id.button_toggle_content); - toggleVisibility = itemView.findViewById(R.id.status_toggle_mute); + toggleVisibility = itemView.findViewById(R.id.status_toggle_mute); } @Override @@ -57,10 +55,11 @@ public class StatusViewHolder extends StatusBaseViewHolder { } @Override - protected void setupWithStatus(StatusViewData.Concrete status, - final StatusActionListener listener, - StatusDisplayOptions statusDisplayOptions, - @Nullable Object payloads) { + protected void setupWithStatus( + StatusViewData.Concrete status, + final StatusActionListener listener, + StatusDisplayOptions statusDisplayOptions, + @Nullable Object payloads) { if (payloads == null) { setupCollapsedState(status, listener); @@ -72,32 +71,36 @@ public class StatusViewHolder extends StatusBaseViewHolder { setRebloggedByDisplayName(rebloggedByDisplayName, status); statusInfo.setOnClickListener(v -> listener.onOpenReblog(getAdapterPosition())); } - - if(status.isUserMuted() || status.isThreadMuted()) { + + if (status.isUserMuted() || status.isThreadMuted()) { toggleVisibility.setVisibility(View.VISIBLE); - toggleVisibility.setOnClickListener(v -> listener.onMute(getAdapterPosition(), true)); + toggleVisibility.setOnClickListener( + v -> listener.onMute(getAdapterPosition(), true)); } else { toggleVisibility.setVisibility(View.GONE); } - } super.setupWithStatus(status, listener, statusDisplayOptions, payloads); - } - private void setRebloggedByDisplayName(final CharSequence name, final StatusViewData.Concrete status) { + private void setRebloggedByDisplayName( + final CharSequence name, final StatusViewData.Concrete status) { Context context = statusInfo.getContext(); CharSequence wrappedName = StringUtils.unicodeWrap(name); CharSequence boostedText = context.getString(R.string.status_boosted_format, wrappedName); - CharSequence emojifiedText = CustomEmojiHelper.emojify(boostedText, status.getRebloggedByAccountEmojis(), statusInfo); + CharSequence emojifiedText = + CustomEmojiHelper.emojify( + boostedText, status.getRebloggedByAccountEmojis(), statusInfo); statusInfo.setText(emojifiedText); statusInfo.setVisibility(View.VISIBLE); } - // don't use this on the same ViewHolder as setRebloggedByDisplayName, will cause recycling issues as paddings are changed + // don't use this on the same ViewHolder as setRebloggedByDisplayName, will cause recycling + // issues as paddings are changed void setPollInfo(final boolean ownPoll) { statusInfo.setText(ownPoll ? R.string.poll_ended_created : R.string.poll_ended_voted); - statusInfo.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_poll_24dp, 0, 0, 0); + statusInfo.setCompoundDrawablesRelativeWithIntrinsicBounds( + R.drawable.ic_poll_24dp, 0, 0, 0); statusInfo.setCompoundDrawablePadding(Utils.dpToPx(statusInfo.getContext(), 10)); statusInfo.setPaddingRelative(Utils.dpToPx(statusInfo.getContext(), 28), 0, 0, 0); statusInfo.setVisibility(View.VISIBLE); @@ -107,14 +110,17 @@ public class StatusViewHolder extends StatusBaseViewHolder { statusInfo.setVisibility(View.GONE); } - private void setupCollapsedState(final StatusViewData.Concrete status, final StatusActionListener listener) { + private void setupCollapsedState( + final StatusViewData.Concrete status, final StatusActionListener listener) { /* input filter for TextViews have to be set before text */ - if (status.isCollapsible() && (status.isExpanded() || TextUtils.isEmpty(status.getSpoilerText()))) { - contentCollapseButton.setOnClickListener(view -> { - int position = getAdapterPosition(); - if (position != RecyclerView.NO_POSITION) - listener.onContentCollapsedChange(!status.isCollapsed(), position); - }); + if (status.isCollapsible() + && (status.isExpanded() || TextUtils.isEmpty(status.getSpoilerText()))) { + contentCollapseButton.setOnClickListener( + view -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) + listener.onContentCollapsedChange(!status.isCollapsed(), position); + }); contentCollapseButton.setVisibility(View.VISIBLE); if (status.isCollapsed()) { diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java index 0143cb4..f9eb3ec 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java @@ -18,16 +18,13 @@ package com.keylesspalace.tusky.adapter; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; - import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.viewdata.StatusViewData; - import java.util.ArrayList; import java.util.List; @@ -52,16 +49,20 @@ public class ThreadAdapter extends RecyclerView.Adapter { public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { switch (viewType) { default: - case VIEW_TYPE_STATUS: { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_status, parent, false); - return new StatusViewHolder(view); - } - case VIEW_TYPE_STATUS_DETAILED: { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_status_detailed, parent, false); - return new StatusDetailedViewHolder(view); - } + case VIEW_TYPE_STATUS: + { + View view = + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_status, parent, false); + return new StatusViewHolder(view); + } + case VIEW_TYPE_STATUS_DETAILED: + { + View view = + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_status_detailed, parent, false); + return new StatusDetailedViewHolder(view); + } } } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java index f5ab042..419eba3 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java @@ -18,16 +18,13 @@ package com.keylesspalace.tusky.adapter; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; - import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.viewdata.StatusViewData; - import java.util.List; public final class TimelineAdapter extends RecyclerView.Adapter { @@ -46,9 +43,10 @@ public final class TimelineAdapter extends RecyclerView.Adapter { private StatusDisplayOptions statusDisplayOptions; private final StatusActionListener statusListener; - public TimelineAdapter(AdapterDataSource<StatusViewData> dataSource, - StatusDisplayOptions statusDisplayOptions, - StatusActionListener statusListener) { + public TimelineAdapter( + AdapterDataSource<StatusViewData> dataSource, + StatusDisplayOptions statusDisplayOptions, + StatusActionListener statusListener) { this.dataSource = dataSource; this.statusDisplayOptions = statusDisplayOptions; this.statusListener = statusListener; @@ -59,17 +57,17 @@ public final class TimelineAdapter extends RecyclerView.Adapter { } public void setMediaPreviewEnabled(boolean mediaPreviewEnabled) { - this.statusDisplayOptions = statusDisplayOptions.copy( - statusDisplayOptions.animateAvatars(), - mediaPreviewEnabled, - statusDisplayOptions.useAbsoluteTime(), - statusDisplayOptions.showBotOverlay(), - statusDisplayOptions.useBlurhash(), - statusDisplayOptions.cardViewMode(), - statusDisplayOptions.confirmReblogs(), - statusDisplayOptions.renderStatusAsMention(), - statusDisplayOptions.hideStats() - ); + this.statusDisplayOptions = + statusDisplayOptions.copy( + statusDisplayOptions.animateAvatars(), + mediaPreviewEnabled, + statusDisplayOptions.useAbsoluteTime(), + statusDisplayOptions.showBotOverlay(), + statusDisplayOptions.useBlurhash(), + statusDisplayOptions.cardViewMode(), + statusDisplayOptions.confirmReblogs(), + statusDisplayOptions.renderStatusAsMention(), + statusDisplayOptions.hideStats()); } @NonNull @@ -77,21 +75,27 @@ public final class TimelineAdapter extends RecyclerView.Adapter { public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) { switch (viewType) { default: - case VIEW_TYPE_STATUS: { - View view = LayoutInflater.from(viewGroup.getContext()) - .inflate(R.layout.item_status, viewGroup, false); - return new StatusViewHolder(view); - } - case VIEW_TYPE_STATUS_MUTED: { - View view = LayoutInflater.from(viewGroup.getContext()) - .inflate(R.layout.item_status_muted, viewGroup, false); - return new MutedStatusViewHolder(view); - } - case VIEW_TYPE_PLACEHOLDER: { - View view = LayoutInflater.from(viewGroup.getContext()) - .inflate(R.layout.item_status_placeholder, viewGroup, false); - return new PlaceholderViewHolder(view); - } + case VIEW_TYPE_STATUS: + { + View view = + LayoutInflater.from(viewGroup.getContext()) + .inflate(R.layout.item_status, viewGroup, false); + return new StatusViewHolder(view); + } + case VIEW_TYPE_STATUS_MUTED: + { + View view = + LayoutInflater.from(viewGroup.getContext()) + .inflate(R.layout.item_status_muted, viewGroup, false); + return new MutedStatusViewHolder(view); + } + case VIEW_TYPE_PLACEHOLDER: + { + View view = + LayoutInflater.from(viewGroup.getContext()) + .inflate(R.layout.item_status_placeholder, viewGroup, false); + return new PlaceholderViewHolder(view); + } } } @@ -100,27 +104,34 @@ public final class TimelineAdapter extends RecyclerView.Adapter { bindViewHolder(viewHolder, position, null); } - @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position, @NonNull List payloads) { + public void onBindViewHolder( + @NonNull RecyclerView.ViewHolder viewHolder, int position, @NonNull List payloads) { bindViewHolder(viewHolder, position, payloads); } - private void bindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position, @Nullable List payloads) { + private void bindViewHolder( + @NonNull RecyclerView.ViewHolder viewHolder, int position, @Nullable List payloads) { StatusViewData status = dataSource.getItemAt(position); if (status instanceof StatusViewData.Placeholder) { PlaceholderViewHolder holder = (PlaceholderViewHolder) viewHolder; holder.setup(statusListener, ((StatusViewData.Placeholder) status).isLoading()); } else if (status instanceof StatusViewData.Concrete) { - StatusViewData.Concrete concrete = (StatusViewData.Concrete)status; - if(concrete.isMuted()) { + StatusViewData.Concrete concrete = (StatusViewData.Concrete) status; + if (concrete.isMuted()) { MutedStatusViewHolder holder = (MutedStatusViewHolder) viewHolder; - holder.setupWithStatus(concrete, statusListener, statusDisplayOptions, - payloads != null && !payloads.isEmpty() ? payloads.get(0) : null); + holder.setupWithStatus( + concrete, + statusListener, + statusDisplayOptions, + payloads != null && !payloads.isEmpty() ? payloads.get(0) : null); } else { StatusViewHolder holder = (StatusViewHolder) viewHolder; - holder.setupWithStatus(concrete, statusListener, statusDisplayOptions, - payloads != null && !payloads.isEmpty() ? payloads.get(0) : null); + holder.setupWithStatus( + concrete, + statusListener, + statusDisplayOptions, + payloads != null && !payloads.isEmpty() ? payloads.get(0) : null); } } } @@ -135,8 +146,9 @@ public final class TimelineAdapter extends RecyclerView.Adapter { if (dataSource.getItemAt(position) instanceof StatusViewData.Placeholder) { return VIEW_TYPE_PLACEHOLDER; } else { - StatusViewData.Concrete concrete = (StatusViewData.Concrete)dataSource.getItemAt(position); - if(concrete.isMuted()) { + StatusViewData.Concrete concrete = + (StatusViewData.Concrete) dataSource.getItemAt(position); + if (concrete.isMuted()) { return VIEW_TYPE_STATUS_MUTED; } else { return VIEW_TYPE_STATUS; diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/UnicodeEmojiAdapter.java b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/UnicodeEmojiAdapter.java index 776fa14..a74ce55 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/UnicodeEmojiAdapter.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/UnicodeEmojiAdapter.java @@ -1,129 +1,129 @@ package com.keylesspalace.tusky.adapter; -import android.view.*; import android.util.*; +import android.view.*; +import androidx.emoji.widget.EmojiAppCompatButton; +import androidx.recyclerview.widget.*; import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayoutMediator; -import com.google.android.flexbox.FlexboxLayoutManager; -import androidx.viewpager2.widget.ViewPager2; -import androidx.recyclerview.widget.*; -import androidx.emoji.widget.EmojiAppCompatButton; -import androidx.emoji.text.EmojiCompat; import com.keylesspalace.tusky.R; -import com.keylesspalace.tusky.view.EmojiKeyboard; import com.keylesspalace.tusky.util.Emojis; +import com.keylesspalace.tusky.view.EmojiKeyboard; import java.util.*; -public class UnicodeEmojiAdapter - extends RecyclerView.Adapter<SingleViewHolder> - implements TabLayoutMediator.TabConfigurationStrategy, EmojiKeyboard.EmojiKeyboardAdapter { - +public class UnicodeEmojiAdapter extends RecyclerView.Adapter<SingleViewHolder> + implements TabLayoutMediator.TabConfigurationStrategy, EmojiKeyboard.EmojiKeyboardAdapter { + private String id; private List<String> recents; private EmojiKeyboard.OnEmojiSelectedListener listener; private RecyclerView recentsView; - - private final static float BUTTON_WIDTH_DP = 65.0f; // empirically found value :( - + + private static final float BUTTON_WIDTH_DP = 65.0f; // empirically found value :( + public UnicodeEmojiAdapter(String id, EmojiKeyboard.OnEmojiSelectedListener listener) { super(); this.id = id; this.listener = listener; } - + @Override public void onConfigureTab(TabLayout.Tab tab, int position) { - if(position == 0) { + if (position == 0) { tab.setIcon(R.drawable.ic_access_time); } else { tab.setText(Emojis.EMOJIS[position - 1][0]); } } - + @Override public int getItemCount() { return Emojis.EMOJIS.length + 1; } - + @Override public void onBindViewHolder(SingleViewHolder holder, int position) { - if(position == 0) { - recentsView = ((RecyclerView)holder.itemView); + if (position == 0) { + recentsView = ((RecyclerView) holder.itemView); recentsView.setAdapter(new UnicodeEmojiPageAdapter(recents, id, listener)); } else { - ((RecyclerView)holder.itemView).setAdapter( - new UnicodeEmojiPageAdapter(Arrays.asList(Emojis.EMOJIS[position - 1]), id, listener)); + ((RecyclerView) holder.itemView) + .setAdapter( + new UnicodeEmojiPageAdapter( + Arrays.asList(Emojis.EMOJIS[position - 1]), id, listener)); } } - + @Override public SingleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_emoji_keyboard_page, parent, false); + View view = + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_emoji_keyboard_page, parent, false); SingleViewHolder holder = new SingleViewHolder(view); - + DisplayMetrics dm = parent.getContext().getResources().getDisplayMetrics(); float wdp = dm.widthPixels / dm.density; int rows = (int) (wdp / BUTTON_WIDTH_DP + 0.5); - - ((RecyclerView)view).setLayoutManager(new GridLayoutManager(view.getContext(), rows)); + + ((RecyclerView) view).setLayoutManager(new GridLayoutManager(view.getContext(), rows)); return holder; } - + @Override public void onRecentsUpdate(Set<String> set) { recents = new ArrayList<String>(set); Collections.reverse(recents); - if(recentsView != null) - recentsView.getAdapter().notifyDataSetChanged(); + if (recentsView != null) recentsView.getAdapter().notifyDataSetChanged(); } - - private abstract class UnicodeEmojiBasePageAdapter extends RecyclerView.Adapter<SingleViewHolder> { + + private abstract class UnicodeEmojiBasePageAdapter + extends RecyclerView.Adapter<SingleViewHolder> { private final EmojiKeyboard.OnEmojiSelectedListener listener; private final String id; - - public UnicodeEmojiBasePageAdapter(String id, EmojiKeyboard.OnEmojiSelectedListener listener) { + + public UnicodeEmojiBasePageAdapter( + String id, EmojiKeyboard.OnEmojiSelectedListener listener) { this.id = id; this.listener = listener; } - - abstract public String getEmoji(int position); - + + public abstract String getEmoji(int position); + @Override public void onBindViewHolder(SingleViewHolder holder, int position) { String emoji = getEmoji(position); - EmojiAppCompatButton btn = (EmojiAppCompatButton)holder.itemView; - + EmojiAppCompatButton btn = (EmojiAppCompatButton) holder.itemView; + btn.setText(emoji); btn.setOnClickListener(v -> listener.onEmojiSelected(id, emoji)); } - + @Override public SingleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_emoji_keyboard_emoji, parent, false); + View view = + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_emoji_keyboard_emoji, parent, false); return new SingleViewHolder(view); } } - + private class UnicodeEmojiPageAdapter extends UnicodeEmojiBasePageAdapter { private final List<String> emojis; - - public UnicodeEmojiPageAdapter(List<String> emojis, String id, EmojiKeyboard.OnEmojiSelectedListener listener) { + + public UnicodeEmojiPageAdapter( + List<String> emojis, String id, EmojiKeyboard.OnEmojiSelectedListener listener) { super(id, listener); this.emojis = emojis; } - + @Override public int getItemCount() { return emojis.size(); } - + @Override public String getEmoji(int position) { return emojis.get(position); } } - } - diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/components/common/DownsizeImageTask.java b/husky/app/src/main/java/com/keylesspalace/tusky/components/common/DownsizeImageTask.java index 17196dd..5ebfc7a 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/components/common/DownsizeImageTask.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/components/common/DownsizeImageTask.java @@ -15,15 +15,16 @@ package com.keylesspalace.tusky.components.common; +import static com.keylesspalace.tusky.util.MediaUtilsKt.calculateInSampleSize; +import static com.keylesspalace.tusky.util.MediaUtilsKt.getImageOrientation; +import static com.keylesspalace.tusky.util.MediaUtilsKt.reorientBitmap; + import android.content.ContentResolver; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.AsyncTask; import com.keylesspalace.tusky.util.IOUtils; -import static com.keylesspalace.tusky.util.MediaUtilsKt.calculateInSampleSize; -import static com.keylesspalace.tusky.util.MediaUtilsKt.getImageOrientation; -import static com.keylesspalace.tusky.util.MediaUtilsKt.reorientBitmap; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; @@ -42,14 +43,13 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> { private File tempFile; /** - * @param sizeLimit the maximum number of bytes each image can take + * @param sizeLimit the maximum number of bytes each image can take * @param contentResolver to resolve the specified images' URIs - * @param tempFile the file where the result will be stored - * @param listener to whom the results are given + * @param tempFile the file where the result will be stored + * @param listener to whom the results are given */ - public DownsizeImageTask(int sizeLimit, ContentResolver contentResolver, File tempFile, - Listener listener) - { + public DownsizeImageTask( + int sizeLimit, ContentResolver contentResolver, File tempFile, Listener listener) { this.sizeLimit = sizeLimit; this.contentResolver = contentResolver; this.tempFile = tempFile; @@ -59,7 +59,7 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> { @Override protected Boolean doInBackground(Uri... uris) { boolean result = DownsizeImageTask.resize(uris, sizeLimit, contentResolver, tempFile); - if(isCancelled()) { + if (isCancelled()) { return false; } return result; @@ -67,7 +67,7 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> { @Override protected void onPostExecute(Boolean successful) { - if(successful) { + if (successful) { listener.onSuccess(tempFile); } else { listener.onFailure(); @@ -75,14 +75,13 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> { super.onPostExecute(successful); } - public static boolean resize(Uri[] uris, long sizeLimit, ContentResolver contentResolver, - File tempFile) - { - for(Uri uri : uris) { + public static boolean resize( + Uri[] uris, long sizeLimit, ContentResolver contentResolver, File tempFile) { + for (Uri uri : uris) { InputStream inputStream; try { inputStream = contentResolver.openInputStream(uri); - } catch(FileNotFoundException e) { + } catch (FileNotFoundException e) { return false; } // Initially, just get the image dimensions. @@ -102,37 +101,37 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> { OutputStream stream; try { stream = new FileOutputStream(tempFile); - } catch(FileNotFoundException e) { + } catch (FileNotFoundException e) { return false; } try { inputStream = contentResolver.openInputStream(uri); - } catch(FileNotFoundException e) { + } catch (FileNotFoundException e) { return false; } options.inSampleSize = - calculateInSampleSize(options, scaledImageSize, scaledImageSize); + calculateInSampleSize(options, scaledImageSize, scaledImageSize); options.inJustDecodeBounds = false; Bitmap scaledBitmap; try { scaledBitmap = BitmapFactory.decodeStream(inputStream, null, options); - } catch(OutOfMemoryError error) { + } catch (OutOfMemoryError error) { return false; } finally { IOUtils.closeQuietly(inputStream); } - if(scaledBitmap == null) { + if (scaledBitmap == null) { return false; } Bitmap reorientedBitmap = reorientBitmap(scaledBitmap, orientation); - if(reorientedBitmap == null) { + if (reorientedBitmap == null) { scaledBitmap.recycle(); return false; } Bitmap.CompressFormat format; /* It's not likely the user will give transparent images over the upload limit, but * if they do, make sure the transparency is retained. */ - if(!reorientedBitmap.hasAlpha()) { + if (!reorientedBitmap.hasAlpha()) { format = Bitmap.CompressFormat.JPEG; } else { format = Bitmap.CompressFormat.PNG; @@ -140,14 +139,12 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> { reorientedBitmap.compress(format, 85, stream); reorientedBitmap.recycle(); scaledImageSize /= 2; - } while(tempFile.length() > sizeLimit); + } while (tempFile.length() > sizeLimit); } return true; } - /** - * Used to communicate the results of the task. - */ + /** Used to communicate the results of the task. */ public interface Listener { void onSuccess(File file); diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.java b/husky/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.java index 09d7068..704a439 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.java @@ -25,7 +25,8 @@ import android.widget.Filter; import android.widget.Filterable; import android.widget.ImageView; import android.widget.TextView; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.bumptech.glide.Glide; import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.entity.Account; @@ -33,19 +34,11 @@ import com.keylesspalace.tusky.entity.Emoji; import com.keylesspalace.tusky.entity.HashTag; import com.keylesspalace.tusky.util.CustomEmojiHelper; import com.keylesspalace.tusky.util.ImageLoadingHelper; - import java.util.ArrayList; import java.util.List; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -/** - * Created by charlag on 12/11/17. - */ - -public class ComposeAutoCompleteAdapter extends BaseAdapter - implements Filterable { +/** Created by charlag on 12/11/17. */ +public class ComposeAutoCompleteAdapter extends BaseAdapter implements Filterable { private static final int ACCOUNT_VIEW_TYPE = 1; private static final int HASHTAG_VIEW_TYPE = 2; private static final int EMOJI_VIEW_TYPE = 3; @@ -85,7 +78,7 @@ public class ComposeAutoCompleteAdapter extends BaseAdapter return formatUsername(((AccountResult) resultValue)); } else if (resultValue instanceof HashtagResult) { return formatHashtag((HashtagResult) resultValue); - } else if (resultValue instanceof EmojiResult) { + } else if (resultValue instanceof EmojiResult) { return formatEmoji((EmojiResult) resultValue); } else { return ""; @@ -129,9 +122,11 @@ public class ComposeAutoCompleteAdapter extends BaseAdapter case ACCOUNT_VIEW_TYPE: AccountViewHolder accountViewHolder; if (convertView == null) { - view = ((LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE)) - .inflate(R.layout.item_autocomplete_account, parent, false); + view = + ((LayoutInflater) + context.getSystemService( + Context.LAYOUT_INFLATER_SERVICE)) + .inflate(R.layout.item_autocomplete_account, parent, false); } if (view.getTag() == null) { view.setTag(new AccountViewHolder(view)); @@ -141,35 +136,44 @@ public class ComposeAutoCompleteAdapter extends BaseAdapter AccountResult accountResult = ((AccountResult) getItem(position)); if (accountResult != null) { Account account = accountResult.account; - String formattedUsername = context.getString( - R.string.status_username_format, - account.getUsername() - ); + String formattedUsername = + context.getString( + R.string.status_username_format, account.getUsername()); accountViewHolder.username.setText(formattedUsername); - CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), - account.getEmojis(), accountViewHolder.displayName); + CharSequence emojifiedName = + CustomEmojiHelper.emojify( + account.getName(), + account.getEmojis(), + accountViewHolder.displayName); accountViewHolder.displayName.setText(emojifiedName); - int avatarRadius = accountViewHolder.avatar.getContext().getResources() - .getDimensionPixelSize(R.dimen.avatar_radius_42dp); + int avatarRadius = + accountViewHolder + .avatar + .getContext() + .getResources() + .getDimensionPixelSize(R.dimen.avatar_radius_42dp); - boolean animateAvatar = PreferenceManager.getDefaultSharedPreferences(accountViewHolder.avatar.getContext()) - .getBoolean("animateGifAvatars", false); + boolean animateAvatar = + PreferenceManager.getDefaultSharedPreferences( + accountViewHolder.avatar.getContext()) + .getBoolean("animateGifAvatars", false); ImageLoadingHelper.loadAvatar( account.getAvatar(), accountViewHolder.avatar, avatarRadius, - animateAvatar - ); + animateAvatar); } break; case HASHTAG_VIEW_TYPE: if (convertView == null) { - view = ((LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE)) - .inflate(R.layout.item_autocomplete_hashtag, parent, false); + view = + ((LayoutInflater) + context.getSystemService( + Context.LAYOUT_INFLATER_SERVICE)) + .inflate(R.layout.item_autocomplete_hashtag, parent, false); } HashtagResult result = (HashtagResult) getItem(position); @@ -181,9 +185,11 @@ public class ComposeAutoCompleteAdapter extends BaseAdapter case EMOJI_VIEW_TYPE: EmojiViewHolder emojiViewHolder; if (convertView == null) { - view = ((LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE)) - .inflate(R.layout.item_autocomplete_emoji, parent, false); + view = + ((LayoutInflater) + context.getSystemService( + Context.LAYOUT_INFLATER_SERVICE)) + .inflate(R.layout.item_autocomplete_emoji, parent, false); } if (view.getTag() == null) { view.setTag(new EmojiViewHolder(view)); @@ -193,10 +199,9 @@ public class ComposeAutoCompleteAdapter extends BaseAdapter EmojiResult emojiResult = ((EmojiResult) getItem(position)); if (emojiResult != null) { Emoji emoji = emojiResult.emoji; - String formattedShortcode = context.getString( - R.string.emoji_shortcode_format, - emoji.getShortcode() - ); + String formattedShortcode = + context.getString( + R.string.emoji_shortcode_format, emoji.getShortcode()); emojiViewHolder.shortcode.setText(formattedShortcode); Glide.with(emojiViewHolder.preview) .load(emoji.getUrl()) @@ -206,9 +211,11 @@ public class ComposeAutoCompleteAdapter extends BaseAdapter case SEPARATOR_VIEW_TYPE: if (convertView == null) { - view = ((LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE)) - .inflate(R.layout.item_autocomplete_divider, parent, false); + view = + ((LayoutInflater) + context.getSystemService( + Context.LAYOUT_INFLATER_SERVICE)) + .inflate(R.layout.item_autocomplete_divider, parent, false); } break; default: @@ -262,11 +269,10 @@ public class ComposeAutoCompleteAdapter extends BaseAdapter } public abstract static class AutocompleteResult { - AutocompleteResult() { - } + AutocompleteResult() {} } - public final static class AccountResult extends AutocompleteResult { + public static final class AccountResult extends AutocompleteResult { public final Account account; public AccountResult(Account account) { @@ -274,7 +280,7 @@ public class ComposeAutoCompleteAdapter extends BaseAdapter } } - public final static class HashtagResult extends AutocompleteResult { + public static final class HashtagResult extends AutocompleteResult { private final String hashtag; public HashtagResult(HashTag hashtag) { @@ -282,7 +288,7 @@ public class ComposeAutoCompleteAdapter extends BaseAdapter } } - public final static class EmojiResult extends AutocompleteResult { + public static final class EmojiResult extends AutocompleteResult { private final Emoji emoji; public EmojiResult(Emoji emoji) { @@ -290,7 +296,7 @@ public class ComposeAutoCompleteAdapter extends BaseAdapter } } - public final static class ResultSeparator extends AutocompleteResult {} + public static final class ResultSeparator extends AutocompleteResult {} public interface AutocompletionProvider { List<AutocompleteResult> search(String mention); diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeScheduleView.java b/husky/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeScheduleView.java index a1a99a7..d6c0b47 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeScheduleView.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeScheduleView.java @@ -21,19 +21,16 @@ import android.os.Bundle; import android.util.AttributeSet; import android.widget.Button; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.content.ContextCompat; - import com.google.android.material.datepicker.CalendarConstraints; import com.google.android.material.datepicker.DateValidatorPointForward; import com.google.android.material.datepicker.MaterialDatePicker; import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.fragment.TimePickerFragment; - import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; @@ -53,7 +50,8 @@ public class ComposeScheduleView extends ConstraintLayout { private TextView invalidScheduleWarningView; private Calendar scheduleDateTime; - public static int MINIMUM_SCHEDULED_SECONDS = 330; // Minimum is 5 minutes, pad 30 seconds for posting + public static int MINIMUM_SCHEDULED_SECONDS = + 330; // Minimum is 5 minutes, pad 30 seconds for posting public ComposeScheduleView(Context context) { super(context); @@ -98,9 +96,9 @@ public class ComposeScheduleView extends ConstraintLayout { invalidScheduleWarningView.setVisibility(GONE); } else { Date scheduled = scheduleDateTime.getTime(); - scheduledDateTimeView.setText(String.format("%s %s", - dateFormat.format(scheduled), - timeFormat.format(scheduled))); + scheduledDateTimeView.setText( + String.format( + "%s %s", dateFormat.format(scheduled), timeFormat.format(scheduled))); verifyScheduledTime(scheduled); } } @@ -129,16 +127,16 @@ public class ComposeScheduleView extends ConstraintLayout { public void openPickDateDialog() { long yesterday = Calendar.getInstance().getTimeInMillis() - 24 * 60 * 60 * 1000; - CalendarConstraints calendarConstraints = new CalendarConstraints.Builder() - .setValidator( - DateValidatorPointForward.from(yesterday)) - .build(); + CalendarConstraints calendarConstraints = + new CalendarConstraints.Builder() + .setValidator(DateValidatorPointForward.from(yesterday)) + .build(); initializeSuggestedTime(); - MaterialDatePicker<Long> picker = MaterialDatePicker.Builder - .datePicker() - .setSelection(scheduleDateTime.getTimeInMillis()) - .setCalendarConstraints(calendarConstraints) - .build(); + MaterialDatePicker<Long> picker = + MaterialDatePicker.Builder.datePicker() + .setSelection(scheduleDateTime.getTimeInMillis()) + .setCalendarConstraints(calendarConstraints) + .build(); picker.addOnPositiveButtonClickListener(this::onDateSet); picker.show(((AppCompatActivity) getContext()).getSupportFragmentManager(), "date_picker"); } @@ -147,8 +145,11 @@ public class ComposeScheduleView extends ConstraintLayout { TimePickerFragment picker = new TimePickerFragment(); if (scheduleDateTime != null) { Bundle args = new Bundle(); - args.putInt(TimePickerFragment.PICKER_TIME_HOUR, scheduleDateTime.get(Calendar.HOUR_OF_DAY)); - args.putInt(TimePickerFragment.PICKER_TIME_MINUTE, scheduleDateTime.get(Calendar.MINUTE)); + args.putInt( + TimePickerFragment.PICKER_TIME_HOUR, + scheduleDateTime.get(Calendar.HOUR_OF_DAY)); + args.putInt( + TimePickerFragment.PICKER_TIME_MINUTE, scheduleDateTime.get(Calendar.MINUTE)); picker.setArguments(args); } picker.show(((AppCompatActivity) getContext()).getSupportFragmentManager(), "time_picker"); @@ -196,7 +197,10 @@ public class ComposeScheduleView extends ConstraintLayout { // see https://github.com/material-components/material-components-android/issues/882 newDate.setTimeZone(TimeZone.getTimeZone("UTC")); newDate.setTimeInMillis(selection); - scheduleDateTime.set(newDate.get(Calendar.YEAR), newDate.get(Calendar.MONTH), newDate.get(Calendar.DATE)); + scheduleDateTime.set( + newDate.get(Calendar.YEAR), + newDate.get(Calendar.MONTH), + newDate.get(Calendar.DATE)); openPickTimeDialog(); } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ProgressImageView.java b/husky/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ProgressImageView.java index 0811703..e7bddec 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ProgressImageView.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ProgressImageView.java @@ -23,14 +23,13 @@ import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.RectF; import android.graphics.drawable.Drawable; +import android.util.AttributeSet; import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.widget.AppCompatImageView; -import android.util.AttributeSet; - -import com.keylesspalace.tusky.R; +import androidx.core.content.ContextCompat; import at.connyduck.sparkbutton.helpers.Utils; +import com.keylesspalace.tusky.R; public final class ProgressImageView extends AppCompatImageView { @@ -65,8 +64,7 @@ public final class ProgressImageView extends AppCompatImageView { clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); markBgPaint.setStyle(Paint.Style.FILL); - markBgPaint.setColor(ContextCompat.getColor(getContext(), - R.color.tusky_grey_10)); + markBgPaint.setColor(ContextCompat.getColor(getContext(), R.color.tusky_grey_10)); captionDrawable = AppCompatResources.getDrawable(getContext(), R.drawable.spellcheck); } @@ -81,8 +79,9 @@ public final class ProgressImageView extends AppCompatImageView { } public void setChecked(boolean checked) { - this.markBgPaint.setColor(ContextCompat.getColor(getContext(), - checked ? R.color.tusky_blue : R.color.tusky_grey_10)); + this.markBgPaint.setColor( + ContextCompat.getColor( + getContext(), checked ? R.color.tusky_blue : R.color.tusky_grey_10)); invalidate(); } @@ -93,10 +92,15 @@ public final class ProgressImageView extends AppCompatImageView { float angle = (progress / 100f) * 360 - 90; float halfWidth = getWidth() / 2.0f; float halfHeight = getHeight() / 2.0f; - progressRect.set(halfWidth * 0.75f, halfHeight * 0.75f, halfWidth * 1.25f, halfHeight * 1.25f); + progressRect.set( + halfWidth * 0.75f, halfHeight * 0.75f, halfWidth * 1.25f, halfHeight * 1.25f); biggerRect.set(progressRect); int margin = 8; - biggerRect.set(progressRect.left - margin, progressRect.top - margin, progressRect.right + margin, progressRect.bottom + margin); + biggerRect.set( + progressRect.left - margin, + progressRect.top - margin, + progressRect.right + margin, + progressRect.bottom + margin); canvas.saveLayer(biggerRect, null, Canvas.ALL_SAVE_FLAG); if (progress != -1) { canvas.drawOval(progressRect, circlePaint); @@ -112,7 +116,8 @@ public final class ProgressImageView extends AppCompatImageView { canvas.drawCircle(circleX, circleY, circleRadius, markBgPaint); - captionDrawable.setBounds(getWidth() - circleMargin - circleRadius, + captionDrawable.setBounds( + getWidth() - circleMargin - circleRadius, getHeight() - circleMargin - circleRadius, getWidth() - circleMargin, getHeight() - circleMargin); diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ProgressTextView.java b/husky/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ProgressTextView.java index 8078f6e..38bed01 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ProgressTextView.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ProgressTextView.java @@ -23,15 +23,13 @@ import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.RectF; import android.graphics.drawable.Drawable; +import android.util.AttributeSet; import android.widget.TextView; import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; import androidx.appcompat.content.res.AppCompatResources; -import androidx.appcompat.widget.AppCompatTextView; -import android.util.AttributeSet; - -import com.keylesspalace.tusky.R; +import androidx.core.content.ContextCompat; import at.connyduck.sparkbutton.helpers.Utils; +import com.keylesspalace.tusky.R; public final class ProgressTextView extends TextView { @@ -66,8 +64,7 @@ public final class ProgressTextView extends TextView { clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); markBgPaint.setStyle(Paint.Style.FILL); - markBgPaint.setColor(ContextCompat.getColor(getContext(), - R.color.tusky_grey_10)); + markBgPaint.setColor(ContextCompat.getColor(getContext(), R.color.tusky_grey_10)); captionDrawable = AppCompatResources.getDrawable(getContext(), R.drawable.spellcheck); } @@ -77,25 +74,31 @@ public final class ProgressTextView extends TextView { } public void setChecked(boolean checked) { - this.markBgPaint.setColor(ContextCompat.getColor(getContext(), - checked ? R.color.tusky_blue : R.color.tusky_grey_10)); + this.markBgPaint.setColor( + ContextCompat.getColor( + getContext(), checked ? R.color.tusky_blue : R.color.tusky_grey_10)); invalidate(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); - + // https://stackoverflow.com/questions/25501185/ - canvas.translate(getScrollX(), 0); - + canvas.translate(getScrollX(), 0); + float angle = (progress / 100f) * 360 - 90; float halfWidth = getWidth() / 2.0f; float halfHeight = getHeight() / 2.0f; - progressRect.set(halfWidth * 0.75f, halfHeight * 0.75f, halfWidth * 1.25f, halfHeight * 1.25f); + progressRect.set( + halfWidth * 0.75f, halfHeight * 0.75f, halfWidth * 1.25f, halfHeight * 1.25f); biggerRect.set(progressRect); int margin = 8; - biggerRect.set(progressRect.left - margin, progressRect.top - margin, progressRect.right + margin, progressRect.bottom + margin); + biggerRect.set( + progressRect.left - margin, + progressRect.top - margin, + progressRect.right + margin, + progressRect.bottom + margin); canvas.saveLayer(biggerRect, null, Canvas.ALL_SAVE_FLAG); if (progress != -1) { canvas.drawOval(progressRect, circlePaint); @@ -111,7 +114,8 @@ public final class ProgressTextView extends TextView { canvas.drawCircle(circleX, circleY, circleRadius, markBgPaint); - captionDrawable.setBounds(getWidth() - circleMargin - circleRadius, + captionDrawable.setBounds( + getWidth() - circleMargin - circleRadius, getHeight() - circleMargin - circleRadius, getWidth() - circleMargin, getHeight() - circleMargin); diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java b/husky/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java index b0e5fd5..5700c5c 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java @@ -36,7 +36,7 @@ import java.util.List; public class ConversationViewHolder extends StatusBaseViewHolder { private static final InputFilter[] COLLAPSE_INPUT_FILTER = - new InputFilter[]{SmartLengthInputFilter.INSTANCE}; + new InputFilter[] {SmartLengthInputFilter.INSTANCE}; private static final InputFilter[] NO_INPUT_FILTER = new InputFilter[0]; private TextView conversationNameTextView; @@ -46,18 +46,22 @@ public class ConversationViewHolder extends StatusBaseViewHolder { private StatusDisplayOptions statusDisplayOptions; private StatusActionListener listener; - ConversationViewHolder(View itemView, StatusDisplayOptions statusDisplayOptions, - StatusActionListener listener) - { + ConversationViewHolder( + View itemView, + StatusDisplayOptions statusDisplayOptions, + StatusActionListener listener) { super(itemView); conversationNameTextView = itemView.findViewById(R.id.conversation_name); contentCollapseButton = itemView.findViewById(R.id.button_toggle_content); - avatars = new ImageView[]{avatar, itemView.findViewById(R.id.status_avatar_1), - itemView.findViewById(R.id.status_avatar_2)}; + avatars = + new ImageView[] { + avatar, + itemView.findViewById(R.id.status_avatar_1), + itemView.findViewById(R.id.status_avatar_2) + }; this.statusDisplayOptions = statusDisplayOptions; this.listener = listener; - } @Override @@ -69,8 +73,12 @@ public class ConversationViewHolder extends StatusBaseViewHolder { ConversationStatusEntity status = conversation.getLastStatus(); ConversationAccountEntity account = status.getAccount(); - setupCollapsedState(status.getCollapsible(), status.getCollapsed(), status.getExpanded(), - status.getSpoilerText(), listener); + setupCollapsedState( + status.getCollapsible(), + status.getCollapsed(), + status.getExpanded(), + status.getSpoilerText(), + listener); setDisplayName(account.getDisplayName(), account.getEmojis()); setUsername(account.getUsername()); @@ -80,15 +88,19 @@ public class ConversationViewHolder extends StatusBaseViewHolder { setBookmarked(status.getBookmarked()); List<Attachment> attachments = status.getAttachments(); boolean sensitive = status.getSensitive(); - if(statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) { - setMediaPreviews(attachments, sensitive, listener, status.getShowingHiddenContent(), - statusDisplayOptions.useBlurhash()); - - if(attachments.size() == 0) { + if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) { + setMediaPreviews( + attachments, + sensitive, + listener, + status.getShowingHiddenContent(), + statusDisplayOptions.useBlurhash()); + + if (attachments.size() == 0) { hideSensitiveMediaWarning(); } // Hide the unused label. - for(TextView mediaLabel : mediaLabels) { + for (TextView mediaLabel : mediaLabels) { mediaLabel.setVisibility(View.GONE); } } else { @@ -101,12 +113,18 @@ public class ConversationViewHolder extends StatusBaseViewHolder { hideSensitiveMediaWarning(); } - setupButtons(listener, account.getId(), status.getContent().toString(), - statusDisplayOptions); + setupButtons( + listener, account.getId(), status.getContent().toString(), statusDisplayOptions); - setSpoilerAndContent(status.getExpanded(), status.getContent(), status.getSpoilerText(), - status.getMentions(), status.getEmojis(), PollViewDataKt.toViewData(status.getPoll()), - statusDisplayOptions, listener); + setSpoilerAndContent( + status.getExpanded(), + status.getContent(), + status.getSpoilerText(), + status.getMentions(), + status.getEmojis(), + PollViewDataKt.toViewData(status.getPoll()), + statusDisplayOptions, + listener); setConversationName(conversation.getAccounts()); @@ -116,27 +134,37 @@ public class ConversationViewHolder extends StatusBaseViewHolder { private void setConversationName(List<ConversationAccountEntity> accounts) { Context context = conversationNameTextView.getContext(); String conversationName = ""; - if(accounts.size() == 1) { - conversationName = context.getString(R.string.conversation_1_recipients, - accounts.get(0).getUsername()); - } else if(accounts.size() == 2) { + if (accounts.size() == 1) { + conversationName = + context.getString( + R.string.conversation_1_recipients, accounts.get(0).getUsername()); + } else if (accounts.size() == 2) { + conversationName = + context.getString( + R.string.conversation_2_recipients, + accounts.get(0).getUsername(), + accounts.get(1).getUsername()); + } else if (accounts.size() > 2) { conversationName = - context.getString(R.string.conversation_2_recipients, accounts.get(0).getUsername(), - accounts.get(1).getUsername()); - } else if(accounts.size() > 2) { - conversationName = context.getString(R.string.conversation_more_recipients, - accounts.get(0).getUsername(), accounts.get(1).getUsername(), accounts.size() - 2); + context.getString( + R.string.conversation_more_recipients, + accounts.get(0).getUsername(), + accounts.get(1).getUsername(), + accounts.size() - 2); } conversationNameTextView.setText(conversationName); } private void setAvatars(List<ConversationAccountEntity> accounts) { - for(int i = 0; i < avatars.length; i++) { + for (int i = 0; i < avatars.length; i++) { ImageView avatarView = avatars[i]; - if(i < accounts.size()) { - ImageLoadingHelper.loadAvatar(accounts.get(i).getAvatar(), avatarView, - avatarRadius48dp, statusDisplayOptions.animateAvatars()); + if (i < accounts.size()) { + ImageLoadingHelper.loadAvatar( + accounts.get(i).getAvatar(), + avatarView, + avatarRadius48dp, + statusDisplayOptions.animateAvatars()); avatarView.setVisibility(View.VISIBLE); } else { avatarView.setVisibility(View.GONE); @@ -144,20 +172,24 @@ public class ConversationViewHolder extends StatusBaseViewHolder { } } - private void setupCollapsedState(boolean collapsible, boolean collapsed, boolean expanded, - String spoilerText, final StatusActionListener listener) - { + private void setupCollapsedState( + boolean collapsible, + boolean collapsed, + boolean expanded, + String spoilerText, + final StatusActionListener listener) { /* input filter for TextViews have to be set before text */ - if(collapsible && (expanded || TextUtils.isEmpty(spoilerText))) { - contentCollapseButton.setOnClickListener(view -> { - int position = getAdapterPosition(); - if(position != RecyclerView.NO_POSITION) { - listener.onContentCollapsedChange(!collapsed, position); - } - }); + if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) { + contentCollapseButton.setOnClickListener( + view -> { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onContentCollapsedChange(!collapsed, position); + } + }); contentCollapseButton.setVisibility(View.VISIBLE); - if(collapsed) { + if (collapsed) { contentCollapseButton.setText(R.string.status_content_warning_show_more); content.setFilters(COLLAPSE_INPUT_FILTER); } else { diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java b/husky/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java index e1540b2..a78227f 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java @@ -23,6 +23,7 @@ package com.keylesspalace.tusky.components.notifications; import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription; + import android.app.NotificationChannel; import android.app.NotificationChannelGroup; import android.app.NotificationManager; @@ -79,9 +80,7 @@ public class NotificationHelper { private static int notificationId = 0; - /** - * constants used in Intents - */ + /** constants used in Intents */ public static final String ACCOUNT_ID = "account_id"; private static final String TAG = "NotificationHelper"; @@ -116,10 +115,9 @@ public class NotificationHelper { public static final String KEY_CHAT_ID = "KEY_CHAT_ID"; - /** - * notification channels used on Android O+ - **/ + /** notification channels used on Android O+ */ public static final String CHANNEL_MENTION = "CHANNEL_MENTION"; + public static final String CHANNEL_FOLLOW = "CHANNEL_FOLLOW"; public static final String CHANNEL_FOLLOW_REQUEST = "CHANNEL_FOLLOW_REQUEST"; public static final String CHANNEL_BOOST = "CHANNEL_BOOST"; @@ -130,41 +128,42 @@ public class NotificationHelper { public static final String CHANNEL_SUBSCRIPTIONS = "CHANNEL_SUBSCRIPTIONS"; public static final String CHANNEL_MOVE = "CHANNEL_MOVE"; - /** - * WorkManager Tag - */ + /** WorkManager Tag */ private static final String NOTIFICATION_PULL_TAG = "pullNotifications"; /** * by setting this as false, it's possible to test legacy notification channels on newer devices */ // public static final boolean NOTIFICATION_USE_CHANNELS = false; - public static final boolean NOTIFICATION_USE_CHANNELS = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; + public static final boolean NOTIFICATION_USE_CHANNELS = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; /** * Takes a given Mastodon notification and either creates a new Android notification or updates * the state of the existing notification to reflect the new interaction. * * @param context to access application preferences and services - * @param body a new Mastodon notification + * @param body a new Mastodon notification * @param account the account for which the notification should be shown */ - - public static void make(final Context context, Notification body, AccountEntity account, boolean isFirstOfBatch) { + public static void make( + final Context context, + Notification body, + AccountEntity account, + boolean isFirstOfBatch) { body = Notification.rewriteToStatusTypeIfNeeded(body, account.getAccountId()); - if(!filterNotification(account, body, context)) { + if (!filterNotification(account, body, context)) { return; } // Pleroma extension: don't notify about seen notifications - if(body.getPleroma() != null && body.getPleroma().getSeen()) { + if (body.getPleroma() != null && body.getPleroma().getSeen()) { return; } - if(body.getStatus() != null && - (body.getStatus().isUserMuted() || - body.getStatus().isThreadMuted())) { + if (body.getStatus() != null + && (body.getStatus().isUserMuted() || body.getStatus().isThreadMuted())) { return; } @@ -173,17 +172,17 @@ public class NotificationHelper { try { currentNotifications = new JSONArray(rawCurrentNotifications); - } catch(JSONException e) { + } catch (JSONException e) { currentNotifications = new JSONArray(); } - for(int i = 0; i < currentNotifications.length(); i++) { + for (int i = 0; i < currentNotifications.length(); i++) { try { - if(currentNotifications.getString(i).equals(body.getAccount().getName())) { + if (currentNotifications.getString(i).equals(body.getAccount().getName())) { currentNotifications.remove(i); break; } - } catch(JSONException e) { + } catch (JSONException e) { Timber.e(e); } } @@ -201,63 +200,78 @@ public class NotificationHelper { builder.setContentTitle(titleForType(context, body, account)) .setContentText(bodyForType(body, context)); - if(body.getType() == Notification.Type.MENTION || body.getType() == Notification.Type.POLL) { - builder.setStyle(new NotificationCompat.BigTextStyle() - .bigText(bodyForType(body, context))); + if (body.getType() == Notification.Type.MENTION + || body.getType() == Notification.Type.POLL) { + builder.setStyle( + new NotificationCompat.BigTextStyle().bigText(bodyForType(body, context))); } - //load the avatar synchronously + // load the avatar synchronously Bitmap accountAvatar; try { - FutureTarget<Bitmap> target = Glide.with(context) - .asBitmap() - .load(body.getAccount().getAvatar()) - .transform(new RoundedCorners(20)) - .submit(); + FutureTarget<Bitmap> target = + Glide.with(context) + .asBitmap() + .load(body.getAccount().getAvatar()) + .transform(new RoundedCorners(20)) + .submit(); accountAvatar = target.get(); - } catch(ExecutionException | InterruptedException e) { + } catch (ExecutionException | InterruptedException e) { Timber.e("Error loading account avatar %s", e); - accountAvatar = BitmapFactory.decodeResource(context.getResources(), R.drawable.avatar_default); + accountAvatar = + BitmapFactory.decodeResource(context.getResources(), R.drawable.avatar_default); } builder.setLargeIcon(accountAvatar); - // Reply to mention action; RemoteInput is available from KitKat Watch, but buttons are available from Nougat - if(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - if(body.getType() == Notification.Type.MENTION) { - RemoteInput replyRemoteInput = new RemoteInput.Builder(KEY_REPLY) - .setLabel(context.getString(R.string.label_quick_reply)) - .build(); + // Reply to mention action; RemoteInput is available from KitKat Watch, but buttons are + // available from Nougat + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (body.getType() == Notification.Type.MENTION) { + RemoteInput replyRemoteInput = + new RemoteInput.Builder(KEY_REPLY) + .setLabel(context.getString(R.string.label_quick_reply)) + .build(); - PendingIntent quickReplyPendingIntent = getStatusReplyIntent(REPLY_ACTION, context, body, account); + PendingIntent quickReplyPendingIntent = + getStatusReplyIntent(REPLY_ACTION, context, body, account); NotificationCompat.Action quickReplyAction = - new NotificationCompat.Action.Builder(R.drawable.ic_reply_24dp, - context.getString(R.string.action_quick_reply), quickReplyPendingIntent) + new NotificationCompat.Action.Builder( + R.drawable.ic_reply_24dp, + context.getString(R.string.action_quick_reply), + quickReplyPendingIntent) .addRemoteInput(replyRemoteInput) .build(); builder.addAction(quickReplyAction); - PendingIntent composePendingIntent = getStatusReplyIntent(COMPOSE_ACTION, context, body, account); + PendingIntent composePendingIntent = + getStatusReplyIntent(COMPOSE_ACTION, context, body, account); NotificationCompat.Action composeAction = - new NotificationCompat.Action.Builder(R.drawable.ic_reply_24dp, - context.getString(R.string.action_compose_shortcut), composePendingIntent) + new NotificationCompat.Action.Builder( + R.drawable.ic_reply_24dp, + context.getString(R.string.action_compose_shortcut), + composePendingIntent) .build(); builder.addAction(composeAction); - } else if(body.getType() == Notification.Type.CHAT_MESSAGE) { - RemoteInput replyRemoteInput = new RemoteInput.Builder(KEY_REPLY) - .setLabel(context.getString(R.string.label_quick_reply)) - .build(); + } else if (body.getType() == Notification.Type.CHAT_MESSAGE) { + RemoteInput replyRemoteInput = + new RemoteInput.Builder(KEY_REPLY) + .setLabel(context.getString(R.string.label_quick_reply)) + .build(); - PendingIntent quickReplyPendingIntent = getStatusReplyIntent(CHAT_REPLY_ACTION, context, body, account); + PendingIntent quickReplyPendingIntent = + getStatusReplyIntent(CHAT_REPLY_ACTION, context, body, account); NotificationCompat.Action quickReplyAction = - new NotificationCompat.Action.Builder(R.drawable.ic_reply_24dp, - context.getString(R.string.action_quick_reply), quickReplyPendingIntent) + new NotificationCompat.Action.Builder( + R.drawable.ic_reply_24dp, + context.getString(R.string.action_quick_reply), + quickReplyPendingIntent) .addRemoteInput(replyRemoteInput) .build(); @@ -271,21 +285,23 @@ public class NotificationHelper { builder.setOnlyAlertOnce(true); // only alert for the first notification of a batch to avoid multiple alerts at once - if(!isFirstOfBatch) { + if (!isFirstOfBatch) { builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY); } // Summary // ======= - final NotificationCompat.Builder summaryBuilder = newNotification(context, body, account, true); + final NotificationCompat.Builder summaryBuilder = + newNotification(context, body, account, true); - if(currentNotifications.length() != 1) { + if (currentNotifications.length() != 1) { try { - String title = context.getString(R.string.notification_title_summary, currentNotifications.length()); + String title = + context.getString( + R.string.notification_title_summary, currentNotifications.length()); String text = joinNames(context, currentNotifications); - summaryBuilder.setContentTitle(title) - .setContentText(text); - } catch(JSONException e) { + summaryBuilder.setContentTitle(title).setContentText(text); + } catch (JSONException e) { Timber.e(e); } } @@ -299,24 +315,26 @@ public class NotificationHelper { NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); notificationManager.notify(notificationId, builder.build()); - if(currentNotifications.length() == 1) { - notificationManager.notify((int) account.getId(), builder.setGroupSummary(true).build()); + if (currentNotifications.length() == 1) { + notificationManager.notify( + (int) account.getId(), builder.setGroupSummary(true).build()); } else { notificationManager.notify((int) account.getId(), summaryBuilder.build()); } } - private static NotificationCompat.Builder newNotification(Context context, Notification body, AccountEntity account, boolean summary) { + private static NotificationCompat.Builder newNotification( + Context context, Notification body, AccountEntity account, boolean summary) { Intent summaryResultIntent = new Intent(context, MainActivity.class); summaryResultIntent.putExtra(ACCOUNT_ID, account.getId()); TaskStackBuilder summaryStackBuilder = TaskStackBuilder.create(context); summaryStackBuilder.addParentStack(MainActivity.class); summaryStackBuilder.addNextIntent(summaryResultIntent); - PendingIntent summaryResultPendingIntent = summaryStackBuilder.getPendingIntent( - (int) (notificationId + account.getId() * 10000), - pendingIntentFlags(false) - ); + PendingIntent summaryResultPendingIntent = + summaryStackBuilder.getPendingIntent( + (int) (notificationId + account.getId() * 10000), + pendingIntentFlags(false)); // we have to switch account here Intent eventResultIntent = new Intent(context, MainActivity.class); @@ -325,41 +343,50 @@ public class NotificationHelper { eventStackBuilder.addParentStack(MainActivity.class); eventStackBuilder.addNextIntent(eventResultIntent); - PendingIntent eventResultPendingIntent = eventStackBuilder.getPendingIntent( - (int) account.getId(), - pendingIntentFlags(false)); + PendingIntent eventResultPendingIntent = + eventStackBuilder.getPendingIntent( + (int) account.getId(), pendingIntentFlags(false)); Intent deleteIntent = new Intent(context, NotificationClearBroadcastReceiver.class); deleteIntent.putExtra(ACCOUNT_ID, account.getId()); - PendingIntent deletePendingIntent = PendingIntent.getBroadcast( - context, summary ? (int) account.getId() : notificationId, - deleteIntent, - pendingIntentFlags(false)); - - NotificationCompat.Builder builder = new NotificationCompat.Builder(context, getChannelId(account, body)) - .setSmallIcon(R.drawable.ic_notify) - .setContentIntent(summary ? summaryResultPendingIntent : eventResultPendingIntent) - .setDeleteIntent(deletePendingIntent) - .setColor(BuildConfig.FLAVOR == "green" ? Color.parseColor("#19A341") : ContextCompat.getColor(context, R.color.tusky_blue)) - .setGroup(account.getAccountId()) - .setAutoCancel(true) - .setShortcutId(Long.toString(account.getId())) - .setDefaults(0); // So it doesn't ring twice, notify only in Target callback + PendingIntent deletePendingIntent = + PendingIntent.getBroadcast( + context, + summary ? (int) account.getId() : notificationId, + deleteIntent, + pendingIntentFlags(false)); + + NotificationCompat.Builder builder = + new NotificationCompat.Builder(context, getChannelId(account, body)) + .setSmallIcon(R.drawable.ic_notify) + .setContentIntent( + summary ? summaryResultPendingIntent : eventResultPendingIntent) + .setDeleteIntent(deletePendingIntent) + .setColor( + BuildConfig.FLAVOR == "green" + ? Color.parseColor("#19A341") + : ContextCompat.getColor(context, R.color.tusky_blue)) + .setGroup(account.getAccountId()) + .setAutoCancel(true) + .setShortcutId(Long.toString(account.getId())) + .setDefaults(0); // So it doesn't ring twice, notify only in Target callback setupPreferences(account, builder); return builder; } - private static PendingIntent getStatusReplyIntent(String action, Context context, Notification body, AccountEntity account) { - Intent replyIntent = new Intent(context, SendStatusBroadcastReceiver.class) - .setAction(action) - .putExtra(KEY_SENDER_ACCOUNT_ID, account.getId()) - .putExtra(KEY_SENDER_ACCOUNT_IDENTIFIER, account.getIdentifier()) - .putExtra(KEY_SENDER_ACCOUNT_FULL_NAME, account.getFullName()) - .putExtra(KEY_NOTIFICATION_ID, notificationId); - - if(action == CHAT_REPLY_ACTION) { + private static PendingIntent getStatusReplyIntent( + String action, Context context, Notification body, AccountEntity account) { + Intent replyIntent = + new Intent(context, SendStatusBroadcastReceiver.class) + .setAction(action) + .putExtra(KEY_SENDER_ACCOUNT_ID, account.getId()) + .putExtra(KEY_SENDER_ACCOUNT_IDENTIFIER, account.getIdentifier()) + .putExtra(KEY_SENDER_ACCOUNT_FULL_NAME, account.getFullName()) + .putExtra(KEY_NOTIFICATION_ID, notificationId); + + if (action == CHAT_REPLY_ACTION) { replyIntent.putExtra(KEY_CHAT_ID, body.getChatMessage().getChatId()); } else { Status status = body.getStatus(); @@ -373,13 +400,14 @@ public class NotificationHelper { Status.Mention[] mentions = actionableStatus.getMentions(); List<String> mentionedUsernames = new ArrayList<>(); mentionedUsernames.add(actionableStatus.getAccount().getUsername()); - for(Status.Mention mention : mentions) { + for (Status.Mention mention : mentions) { mentionedUsernames.add(mention.getUsername()); } mentionedUsernames.removeAll(Collections.singleton(account.getUsername())); mentionedUsernames = new ArrayList<>(new LinkedHashSet<>(mentionedUsernames)); - replyIntent.putExtra(KEY_CITED_AUTHOR_LOCAL, citedLocalAuthor) + replyIntent + .putExtra(KEY_CITED_AUTHOR_LOCAL, citedLocalAuthor) .putExtra(KEY_CITED_TEXT, citedText) .putExtra(KEY_CITED_STATUS_ID, inReplyToId) .putExtra(KEY_VISIBILITY, replyVisibility) @@ -387,63 +415,68 @@ public class NotificationHelper { .putExtra(KEY_MENTIONS, mentionedUsernames.toArray(new String[0])); } - return PendingIntent.getBroadcast(context.getApplicationContext(), + return PendingIntent.getBroadcast( + context.getApplicationContext(), notificationId, replyIntent, pendingIntentFlags(true)); } - public static void createNotificationChannelsForAccount(@NonNull AccountEntity account, @NonNull Context context) { - if(NOTIFICATION_USE_CHANNELS) { - NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - - String[] channelIds = new String[]{ - CHANNEL_MENTION + account.getIdentifier(), - CHANNEL_FOLLOW + account.getIdentifier(), - CHANNEL_FOLLOW_REQUEST + account.getIdentifier(), - CHANNEL_BOOST + account.getIdentifier(), - CHANNEL_FAVOURITE + account.getIdentifier(), - CHANNEL_POLL + account.getIdentifier(), - CHANNEL_EMOJI_REACTION + account.getIdentifier(), - CHANNEL_CHAT_MESSAGES + account.getIdentifier(), - CHANNEL_SUBSCRIPTIONS + account.getIdentifier(), - CHANNEL_MOVE + account.getIdentifier() - }; + public static void createNotificationChannelsForAccount( + @NonNull AccountEntity account, @NonNull Context context) { + if (NOTIFICATION_USE_CHANNELS) { + NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + + String[] channelIds = + new String[] { + CHANNEL_MENTION + account.getIdentifier(), + CHANNEL_FOLLOW + account.getIdentifier(), + CHANNEL_FOLLOW_REQUEST + account.getIdentifier(), + CHANNEL_BOOST + account.getIdentifier(), + CHANNEL_FAVOURITE + account.getIdentifier(), + CHANNEL_POLL + account.getIdentifier(), + CHANNEL_EMOJI_REACTION + account.getIdentifier(), + CHANNEL_CHAT_MESSAGES + account.getIdentifier(), + CHANNEL_SUBSCRIPTIONS + account.getIdentifier(), + CHANNEL_MOVE + account.getIdentifier() + }; int[] channelNames = { - R.string.notification_mention_name, - R.string.notification_follow_name, - R.string.notification_follow_request_name, - R.string.notification_boost_name, - R.string.notification_favourite_name, - R.string.notification_poll_name, - R.string.notification_emoji_name, - R.string.notification_chat_message_name, - R.string.notification_subscription_name, - R.string.notification_move_name + R.string.notification_mention_name, + R.string.notification_follow_name, + R.string.notification_follow_request_name, + R.string.notification_boost_name, + R.string.notification_favourite_name, + R.string.notification_poll_name, + R.string.notification_emoji_name, + R.string.notification_chat_message_name, + R.string.notification_subscription_name, + R.string.notification_move_name }; int[] channelDescriptions = { - R.string.notification_mention_descriptions, - R.string.notification_follow_description, - R.string.notification_follow_request_description, - R.string.notification_boost_description, - R.string.notification_favourite_description, - R.string.notification_poll_description, - R.string.notification_emoji_description, - R.string.notification_chat_message_description, - R.string.notification_subscription_description, - R.string.notification_move_description + R.string.notification_mention_descriptions, + R.string.notification_follow_description, + R.string.notification_follow_request_description, + R.string.notification_boost_description, + R.string.notification_favourite_description, + R.string.notification_poll_description, + R.string.notification_emoji_description, + R.string.notification_chat_message_description, + R.string.notification_subscription_description, + R.string.notification_move_description }; List<NotificationChannel> channels = new ArrayList<>(9); - NotificationChannelGroup channelGroup = new NotificationChannelGroup(account.getIdentifier(), account.getFullName()); + NotificationChannelGroup channelGroup = + new NotificationChannelGroup(account.getIdentifier(), account.getFullName()); //noinspection ConstantConditions notificationManager.createNotificationChannelGroup(channelGroup); - for(int i = 0; i < channelIds.length; i++) { + for (int i = 0; i < channelIds.length; i++) { String id = channelIds[i]; String name = context.getString(channelNames[i]); String description = context.getString(channelDescriptions[i]); @@ -463,18 +496,22 @@ public class NotificationHelper { } } - public static void deleteNotificationChannelsForAccount(@NonNull AccountEntity account, @NonNull Context context) { - if(NOTIFICATION_USE_CHANNELS) { - NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + public static void deleteNotificationChannelsForAccount( + @NonNull AccountEntity account, @NonNull Context context) { + if (NOTIFICATION_USE_CHANNELS) { + NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); //noinspection ConstantConditions notificationManager.deleteNotificationChannelGroup(account.getIdentifier()); } } - public static void deleteLegacyNotificationChannels(@NonNull Context context, @NonNull AccountManager accountManager) { - if(NOTIFICATION_USE_CHANNELS) { - NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + public static void deleteLegacyNotificationChannels( + @NonNull Context context, @NonNull AccountManager accountManager) { + if (NOTIFICATION_USE_CHANNELS) { + NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); // used until Tusky 1.4 //noinspection ConstantConditions @@ -484,21 +521,24 @@ public class NotificationHelper { notificationManager.deleteNotificationChannel(CHANNEL_FOLLOW); // used until Tusky 1.7 - for(AccountEntity account : accountManager.getAllAccountsOrderedByActive()) { - notificationManager.deleteNotificationChannel(CHANNEL_FAVOURITE + " " + account.getIdentifier()); + for (AccountEntity account : accountManager.getAllAccountsOrderedByActive()) { + notificationManager.deleteNotificationChannel( + CHANNEL_FAVOURITE + " " + account.getIdentifier()); } } } - public static boolean areNotificationsEnabled(@NonNull Context context, @NonNull AccountManager accountManager) { - if(NOTIFICATION_USE_CHANNELS) { + public static boolean areNotificationsEnabled( + @NonNull Context context, @NonNull AccountManager accountManager) { + if (NOTIFICATION_USE_CHANNELS) { // on Android >= O, notifications are enabled, if at least one channel is enabled - NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); //noinspection ConstantConditions - if(notificationManager.areNotificationsEnabled()) { - for(NotificationChannel channel : notificationManager.getNotificationChannels()) { - if(channel.getImportance() > NotificationManager.IMPORTANCE_NONE) { + if (notificationManager.areNotificationsEnabled()) { + for (NotificationChannel channel : notificationManager.getNotificationChannels()) { + if (channel.getImportance() > NotificationManager.IMPORTANCE_NONE) { Timber.d("NotificationsEnabled"); return true; } @@ -508,7 +548,8 @@ public class NotificationHelper { return false; } else { - // on Android < O, notifications are enabled, if at least one account has notification enabled + // on Android < O, notifications are enabled, if at least one account has notification + // enabled return accountManager.areNotificationsEnabled(); } } @@ -517,18 +558,25 @@ public class NotificationHelper { WorkManager workManager = WorkManager.getInstance(context); workManager.cancelAllWorkByTag(NOTIFICATION_PULL_TAG); - WorkRequest workRequest = new PeriodicWorkRequest.Builder( - NotificationWorker.class, - PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS, TimeUnit.MILLISECONDS, - PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS, TimeUnit.MILLISECONDS - ) - .addTag(NOTIFICATION_PULL_TAG) - .setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()) - .build(); + WorkRequest workRequest = + new PeriodicWorkRequest.Builder( + NotificationWorker.class, + PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS, + TimeUnit.MILLISECONDS, + PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS, + TimeUnit.MILLISECONDS) + .addTag(NOTIFICATION_PULL_TAG) + .setConstraints( + new Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build()) + .build(); workManager.enqueue(workRequest); - Timber.d("enabled notification checks with ${PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS} ms interval"); + Timber.d( + "enabled notification checks with" + + " ${PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS} ms interval"); } public static void disablePullNotifications(Context context) { @@ -536,30 +584,36 @@ public class NotificationHelper { Timber.d("Disabled notification checks"); } - public static void clearNotificationsForActiveAccount(@NonNull Context context, @NonNull AccountManager accountManager) { + public static void clearNotificationsForActiveAccount( + @NonNull Context context, @NonNull AccountManager accountManager) { AccountEntity account = accountManager.getActiveAccount(); - if(account != null && !account.getActiveNotifications().equals("[]")) { - Single.fromCallable(() -> { - account.setActiveNotifications("[]"); - accountManager.saveAccount(account); - - NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - //noinspection ConstantConditions - notificationManager.cancel((int) account.getId()); - return true; - }) + if (account != null && !account.getActiveNotifications().equals("[]")) { + Single.fromCallable( + () -> { + account.setActiveNotifications("[]"); + accountManager.saveAccount(account); + + NotificationManager notificationManager = + (NotificationManager) + context.getSystemService( + Context.NOTIFICATION_SERVICE); + //noinspection ConstantConditions + notificationManager.cancel((int) account.getId()); + return true; + }) .subscribeOn(Schedulers.io()) .subscribe(); } } - private static boolean filterNotification(AccountEntity account, Notification notification, - Context context) { - if(NOTIFICATION_USE_CHANNELS) { - NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + private static boolean filterNotification( + AccountEntity account, Notification notification, Context context) { + if (NOTIFICATION_USE_CHANNELS) { + NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); String channelId = getChannelId(account, notification); - if(channelId == null) { + if (channelId == null) { // unknown notificationtype return false; } @@ -568,7 +622,7 @@ public class NotificationHelper { return channel.getImportance() > NotificationManager.IMPORTANCE_NONE; } - switch(notification.getType()) { + switch (notification.getType()) { case MENTION: return account.getNotificationsMentioned(); case STATUS: @@ -596,7 +650,7 @@ public class NotificationHelper { @Nullable private static String getChannelId(AccountEntity account, Notification notification) { - switch(notification.getType()) { + switch (notification.getType()) { case MENTION: return CHANNEL_MENTION + account.getIdentifier(); case STATUS: @@ -623,21 +677,20 @@ public class NotificationHelper { } private static void setupPreferences( - AccountEntity account, - NotificationCompat.Builder builder) { - if(NOTIFICATION_USE_CHANNELS) { - return; //do nothing on Android O or newer, the system uses the channel settings anyway + AccountEntity account, NotificationCompat.Builder builder) { + if (NOTIFICATION_USE_CHANNELS) { + return; // do nothing on Android O or newer, the system uses the channel settings anyway } - if(account.getNotificationSound()) { + if (account.getNotificationSound()) { builder.setSound(Settings.System.DEFAULT_NOTIFICATION_URI); } - if(account.getNotificationVibration()) { - builder.setVibrate(new long[]{500, 500}); + if (account.getNotificationVibration()) { + builder.setVibrate(new long[] {500, 500}); } - if(account.getNotificationLight()) { + if (account.getNotificationLight()) { builder.setLights(0xFF2B90D9, 300, 1000); } } @@ -648,20 +701,23 @@ public class NotificationHelper { @Nullable private static String joinNames(Context context, JSONArray array) throws JSONException { - if(array.length() > 3) { + if (array.length() > 3) { int length = array.length(); - return String.format(context.getString(R.string.notification_summary_large), + return String.format( + context.getString(R.string.notification_summary_large), 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), + } else if (array.length() == 3) { + return String.format( + context.getString(R.string.notification_summary_medium), wrapItemAt(array, 2), wrapItemAt(array, 1), wrapItemAt(array, 0)); - } else if(array.length() == 2) { - return String.format(context.getString(R.string.notification_summary_small), + } else if (array.length() == 2) { + return String.format( + context.getString(R.string.notification_summary_small), wrapItemAt(array, 1), wrapItemAt(array, 0)); } @@ -670,49 +726,55 @@ public class NotificationHelper { } @Nullable - private static String titleForType(Context context, Notification notification, AccountEntity account) { + private static String titleForType( + Context context, Notification notification, AccountEntity account) { String accountName = StringUtils.unicodeWrap(notification.getAccount().getName()); - switch(notification.getType()) { + switch (notification.getType()) { case MENTION: - return String.format(context.getString(R.string.notification_mention_format), - accountName); + return String.format( + context.getString(R.string.notification_mention_format), accountName); case STATUS: - return String.format(context.getString(R.string.notification_subscription_format), - accountName); + return String.format( + context.getString(R.string.notification_subscription_format), accountName); case FOLLOW: - return String.format(context.getString(R.string.notification_follow_format), - accountName); + return String.format( + context.getString(R.string.notification_follow_format), accountName); case FOLLOW_REQUEST: - return String.format(context.getString(R.string.notification_follow_request_format), + return String.format( + context.getString(R.string.notification_follow_request_format), accountName); case FAVOURITE: - return String.format(context.getString(R.string.notification_favourite_format), - accountName); + return String.format( + context.getString(R.string.notification_favourite_format), accountName); case REBLOG: - return String.format(context.getString(R.string.notification_reblog_format), - accountName); + return String.format( + context.getString(R.string.notification_reblog_format), accountName); case EMOJI_REACTION: - return String.format(context.getString(R.string.notification_emoji_format), - accountName, notification.getEmoji()); + return String.format( + context.getString(R.string.notification_emoji_format), + accountName, + notification.getEmoji()); case POLL: - if(notification.getStatus().getAccount().getId().equals(account.getAccountId())) { + if (notification.getStatus().getAccount().getId().equals(account.getAccountId())) { return context.getString(R.string.poll_ended_created); } else { return context.getString(R.string.poll_ended_voted); } case CHAT_MESSAGE: - return String.format(context.getString(R.string.notification_chat_message_format), - accountName); - case MOVE: { - return String.format(context.getString(R.string.notification_move_format), accountName); - } + return String.format( + context.getString(R.string.notification_chat_message_format), accountName); + case MOVE: + { + return String.format( + context.getString(R.string.notification_move_format), accountName); + } } return null; } private static String bodyForType(Notification notification, Context context) { - switch(notification.getType()) { + switch (notification.getType()) { case MOVE: return "@" + notification.getTarget().getUsername(); case FOLLOW: @@ -723,32 +785,37 @@ public class NotificationHelper { case REBLOG: case EMOJI_REACTION: case STATUS: - if(!TextUtils.isEmpty(notification.getStatus().getSpoilerText())) { + if (!TextUtils.isEmpty(notification.getStatus().getSpoilerText())) { return notification.getStatus().getSpoilerText(); } else { return notification.getStatus().getContent().toString(); } case POLL: - if(!TextUtils.isEmpty(notification.getStatus().getSpoilerText())) { + if (!TextUtils.isEmpty(notification.getStatus().getSpoilerText())) { return notification.getStatus().getSpoilerText(); } else { - StringBuilder builder = new StringBuilder(notification.getStatus().getContent()); + StringBuilder builder = + new StringBuilder(notification.getStatus().getContent()); builder.append('\n'); Poll poll = notification.getStatus().getPoll(); - for(PollOption option : poll.getOptions()) { - builder.append(buildDescription(option.getTitle(), - PollViewDataKt.calculatePercent(option.getVotesCount(), poll.getVotesCount()), - context)); + for (PollOption option : poll.getOptions()) { + builder.append( + buildDescription( + option.getTitle(), + PollViewDataKt.calculatePercent( + option.getVotesCount(), poll.getVotesCount()), + context)); builder.append('\n'); } return builder.toString(); } case CHAT_MESSAGE: - if(!TextUtils.isEmpty(notification.getChatMessage().getContent())) { + if (!TextUtils.isEmpty(notification.getChatMessage().getContent())) { return notification.getChatMessage().getContent().toString(); - } else if(notification.getChatMessage().getAttachment() != null) { - return context.getString(notification.getChatMessage().getAttachment().describeAttachmentType()); - } else if(notification.getChatMessage().getCard() != null) { + } else if (notification.getChatMessage().getAttachment() != null) { + return context.getString( + notification.getChatMessage().getAttachment().describeAttachmentType()); + } else if (notification.getChatMessage().getCard() != null) { return context.getString(R.string.link); } else { return ""; @@ -759,8 +826,8 @@ public class NotificationHelper { public static int pendingIntentFlags(boolean mutable) { return (PendingIntent.FLAG_UPDATE_CURRENT - | (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? - (mutable ? PendingIntent.FLAG_MUTABLE : PendingIntent.FLAG_IMMUTABLE) : 0) - ); + | (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S + ? (mutable ? PendingIntent.FLAG_MUTABLE : PendingIntent.FLAG_IMMUTABLE) + : 0)); } } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/core/utils/image/SvgModule.java b/husky/app/src/main/java/com/keylesspalace/tusky/core/utils/image/SvgModule.java index 08fc1b9..b07ba61 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/core/utils/image/SvgModule.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/core/utils/image/SvgModule.java @@ -29,17 +29,13 @@ import com.bumptech.glide.module.LibraryGlideModule; import com.caverock.androidsvg.SVG; import java.io.InputStream; -/** - * Module for adding SVG decoding support to Glide. - */ +/** Module for adding SVG decoding support to Glide. */ @GlideModule public class SvgModule extends LibraryGlideModule { @Override public void registerComponents( - @NonNull Context context, - @NonNull Glide glide, - @NonNull Registry registry) { + @NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { registry.register(SVG.class, Drawable.class, new SvgDrawableTranscoder(context)) .append(InputStream.class, SVG.class, new SvgDecoder()); } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java b/husky/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java index e92b63e..7965789 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java @@ -20,397 +20,486 @@ import androidx.room.Database; import androidx.room.RoomDatabase; import androidx.room.migration.Migration; import androidx.sqlite.db.SupportSQLiteDatabase; - import com.keylesspalace.tusky.TabDataKt; import com.keylesspalace.tusky.components.conversation.ConversationEntity; -/** - * DB version & declare DAO - */ - -@Database(entities = { TootEntity.class, DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class, - TimelineAccountEntity.class, ConversationEntity.class, ChatEntity.class, ChatMessageEntity.class - }, version = 28) +/** DB version & declare DAO */ +@Database( + entities = { + TootEntity.class, + DraftEntity.class, + AccountEntity.class, + InstanceEntity.class, + TimelineStatusEntity.class, + TimelineAccountEntity.class, + ConversationEntity.class, + ChatEntity.class, + ChatMessageEntity.class + }, + version = 28) public abstract class AppDatabase extends RoomDatabase { public abstract TootDao tootDao(); + public abstract AccountDao accountDao(); + public abstract InstanceDao instanceDao(); + public abstract ConversationsDao conversationDao(); + public abstract TimelineDao timelineDao(); + public abstract ChatsDao chatsDao(); + public abstract DraftDao draftDao(); - public static final Migration MIGRATION_2_3 = new Migration(2, 3) { - @Override - public void migrate(@NonNull SupportSQLiteDatabase database) { - database.execSQL("CREATE TABLE TootEntity2 (uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, text TEXT, urls TEXT, contentWarning TEXT);"); - database.execSQL("INSERT INTO TootEntity2 SELECT * FROM TootEntity;"); - database.execSQL("DROP TABLE TootEntity;"); - database.execSQL("ALTER TABLE TootEntity2 RENAME TO TootEntity;"); - } - }; - - public static final Migration MIGRATION_3_4 = new Migration(3, 4) { - @Override - public void migrate(@NonNull SupportSQLiteDatabase database) { - database.execSQL("ALTER TABLE TootEntity ADD COLUMN inReplyToId TEXT"); - database.execSQL("ALTER TABLE TootEntity ADD COLUMN inReplyToText TEXT"); - database.execSQL("ALTER TABLE TootEntity ADD COLUMN inReplyToUsername TEXT"); - database.execSQL("ALTER TABLE TootEntity ADD COLUMN visibility INTEGER"); - } - }; - - public static final Migration MIGRATION_4_5 = new Migration(4, 5) { - @Override - public void migrate(@NonNull SupportSQLiteDatabase database) { - database.execSQL("CREATE TABLE `AccountEntity` (" + - "`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + - "`domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, " + - "`isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, " + - "`username` TEXT NOT NULL, `displayName` TEXT NOT NULL, " + - "`profilePictureUrl` TEXT NOT NULL, " + - "`notificationsEnabled` INTEGER NOT NULL, " + - "`notificationsMentioned` INTEGER NOT NULL, " + - "`notificationsFollowed` INTEGER NOT NULL, " + - "`notificationsReblogged` INTEGER NOT NULL, " + - "`notificationsFavorited` INTEGER NOT NULL, " + - "`notificationSound` INTEGER NOT NULL, " + - "`notificationVibration` INTEGER NOT NULL, " + - "`notificationLight` INTEGER NOT NULL, " + - "`lastNotificationId` TEXT NOT NULL, " + - "`activeNotifications` TEXT NOT NULL)"); - database.execSQL("CREATE UNIQUE INDEX `index_AccountEntity_domain_accountId` ON `AccountEntity` (`domain`, `accountId`)"); - } - }; - - public static final Migration MIGRATION_5_6 = new Migration(5, 6) { - @Override - public void migrate(@NonNull SupportSQLiteDatabase database) { - database.execSQL("CREATE TABLE IF NOT EXISTS `EmojiListEntity` (`instance` TEXT NOT NULL, `emojiList` TEXT NOT NULL, PRIMARY KEY(`instance`))"); - } - }; - - public static final Migration MIGRATION_6_7 = new Migration(6, 7) { - @Override - public void migrate(@NonNull SupportSQLiteDatabase database) { - database.execSQL("CREATE TABLE IF NOT EXISTS `InstanceEntity` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, PRIMARY KEY(`instance`))"); - database.execSQL("INSERT OR REPLACE INTO `InstanceEntity` SELECT `instance`,`emojiList`, NULL FROM `EmojiListEntity`;"); - database.execSQL("DROP TABLE `EmojiListEntity`;"); - } - }; - - public static final Migration MIGRATION_7_8 = new Migration(7, 8) { - @Override - public void migrate(@NonNull SupportSQLiteDatabase database) { - database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `emojis` TEXT NOT NULL DEFAULT '[]'"); - } - }; - - public static final Migration MIGRATION_8_9 = new Migration(8, 9) { - @Override - public void migrate(@NonNull SupportSQLiteDatabase database) { - database.execSQL("ALTER TABLE `TootEntity` ADD COLUMN `descriptions` TEXT DEFAULT '[]'"); - } - }; - - public static final Migration MIGRATION_9_10 = new Migration(9, 10) { - @Override - public void migrate(@NonNull SupportSQLiteDatabase database) { - database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `defaultPostPrivacy` INTEGER NOT NULL DEFAULT 1"); - database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `defaultMediaSensitivity` INTEGER NOT NULL DEFAULT 0"); - database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `alwaysShowSensitiveMedia` INTEGER NOT NULL DEFAULT 0"); - database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `mediaPreviewEnabled` INTEGER NOT NULL DEFAULT '1'"); - } - }; - - public static final Migration MIGRATION_10_11 = new Migration(10, 11) { - @Override - public void migrate(@NonNull SupportSQLiteDatabase database) { - database.execSQL("CREATE TABLE IF NOT EXISTS `TimelineAccountEntity` (" + - "`serverId` TEXT NOT NULL, " + - "`timelineUserId` INTEGER NOT NULL, " + - "`instance` TEXT NOT NULL, " + - "`localUsername` TEXT NOT NULL, " + - "`username` TEXT NOT NULL, " + - "`displayName` TEXT NOT NULL, " + - "`url` TEXT NOT NULL, " + - "`avatar` TEXT NOT NULL, " + - "`emojis` TEXT NOT NULL," + - "PRIMARY KEY(`serverId`, `timelineUserId`))"); - - database.execSQL("CREATE TABLE IF NOT EXISTS `TimelineStatusEntity` (" + - "`serverId` TEXT NOT NULL, " + - "`url` TEXT, " + - "`timelineUserId` INTEGER NOT NULL, " + - "`authorServerId` TEXT," + - "`instance` TEXT, " + - "`inReplyToId` TEXT, " + - "`inReplyToAccountId` TEXT, " + - "`content` TEXT, " + - "`createdAt` INTEGER NOT NULL, " + - "`emojis` TEXT, " + - "`reblogsCount` INTEGER NOT NULL, " + - "`favouritesCount` INTEGER NOT NULL, " + - "`reblogged` INTEGER NOT NULL, " + - "`favourited` INTEGER NOT NULL, " + - "`sensitive` INTEGER NOT NULL, " + - "`spoilerText` TEXT, " + - "`visibility` INTEGER, " + - "`attachments` TEXT, " + - "`mentions` TEXT, " + - "`application` TEXT, " + - "`reblogServerId` TEXT, " + - "`reblogAccountId` TEXT," + - " PRIMARY KEY(`serverId`, `timelineUserId`)," + - " FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) " + - "ON UPDATE NO ACTION ON DELETE NO ACTION )"); - database.execSQL("CREATE INDEX IF NOT EXISTS" + - "`index_TimelineStatusEntity_authorServerId_timelineUserId` " + - "ON `TimelineStatusEntity` (`authorServerId`, `timelineUserId`)"); - } - }; - - public static final Migration MIGRATION_11_12 = new Migration(11, 12) { - @Override - public void migrate(@NonNull SupportSQLiteDatabase database) { - String defaultTabs = TabDataKt.HOME + ";" + - TabDataKt.NOTIFICATIONS + ";" + - TabDataKt.LOCAL + ";" + - TabDataKt.FEDERATED; - database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `tabPreferences` TEXT NOT NULL DEFAULT '" + defaultTabs + "'"); - - database.execSQL("CREATE TABLE IF NOT EXISTS `ConversationEntity` (" + - "`accountId` INTEGER NOT NULL, " + - "`id` TEXT NOT NULL, " + - "`accounts` TEXT NOT NULL, " + - "`unread` INTEGER NOT NULL, " + - "`s_id` TEXT NOT NULL, " + - "`s_url` TEXT, " + - "`s_inReplyToId` TEXT, " + - "`s_inReplyToAccountId` TEXT, " + - "`s_account` TEXT NOT NULL, " + - "`s_content` TEXT NOT NULL, " + - "`s_createdAt` INTEGER NOT NULL, " + - "`s_emojis` TEXT NOT NULL, " + - "`s_favouritesCount` INTEGER NOT NULL, " + - "`s_favourited` INTEGER NOT NULL, " + - "`s_sensitive` INTEGER NOT NULL, " + - "`s_spoilerText` TEXT NOT NULL, " + - "`s_attachments` TEXT NOT NULL, " + - "`s_mentions` TEXT NOT NULL, " + - "`s_showingHiddenContent` INTEGER NOT NULL, " + - "`s_expanded` INTEGER NOT NULL, " + - "`s_collapsible` INTEGER NOT NULL, " + - "`s_collapsed` INTEGER NOT NULL, " + - "PRIMARY KEY(`id`, `accountId`))"); - - } - }; - - public static final Migration MIGRATION_12_13 = new Migration(12, 13) { - @Override - public void migrate(@NonNull SupportSQLiteDatabase database) { - - database.execSQL("DROP TABLE IF EXISTS `TimelineAccountEntity`"); - database.execSQL("DROP TABLE IF EXISTS `TimelineStatusEntity`"); - - database.execSQL("CREATE TABLE IF NOT EXISTS `TimelineAccountEntity` (" + - "`serverId` TEXT NOT NULL, " + - "`timelineUserId` INTEGER NOT NULL, " + - "`localUsername` TEXT NOT NULL, " + - "`username` TEXT NOT NULL, " + - "`displayName` TEXT NOT NULL, " + - "`url` TEXT NOT NULL, " + - "`avatar` TEXT NOT NULL, " + - "`emojis` TEXT NOT NULL," + - "PRIMARY KEY(`serverId`, `timelineUserId`))"); - - database.execSQL("CREATE TABLE IF NOT EXISTS `TimelineStatusEntity` (" + - "`serverId` TEXT NOT NULL, " + - "`url` TEXT, " + - "`timelineUserId` INTEGER NOT NULL, " + - "`authorServerId` TEXT," + - "`inReplyToId` TEXT, " + - "`inReplyToAccountId` TEXT, " + - "`content` TEXT, " + - "`createdAt` INTEGER NOT NULL, " + - "`emojis` TEXT, " + - "`reblogsCount` INTEGER NOT NULL, " + - "`favouritesCount` INTEGER NOT NULL, " + - "`reblogged` INTEGER NOT NULL, " + - "`favourited` INTEGER NOT NULL, " + - "`sensitive` INTEGER NOT NULL, " + - "`spoilerText` TEXT, " + - "`visibility` INTEGER, " + - "`attachments` TEXT, " + - "`mentions` TEXT, " + - "`application` TEXT, " + - "`reblogServerId` TEXT, " + - "`reblogAccountId` TEXT," + - " PRIMARY KEY(`serverId`, `timelineUserId`)," + - " FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) " + - "ON UPDATE NO ACTION ON DELETE NO ACTION )"); - database.execSQL("CREATE INDEX IF NOT EXISTS" + - "`index_TimelineStatusEntity_authorServerId_timelineUserId` " + - "ON `TimelineStatusEntity` (`authorServerId`, `timelineUserId`)"); - } - }; - - public static final Migration MIGRATION_10_13 = new Migration(10, 13) { - @Override - public void migrate(@NonNull SupportSQLiteDatabase database) { - MIGRATION_11_12.migrate(database); - MIGRATION_12_13.migrate(database); - } - }; - - public static final Migration MIGRATION_13_14 = new Migration(13, 14) { - @Override - public void migrate(@NonNull SupportSQLiteDatabase database) { - database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsFilter` TEXT NOT NULL DEFAULT '[]'"); - } - }; - - public static final Migration MIGRATION_14_15 = new Migration(14, 15) { - @Override - public void migrate(@NonNull SupportSQLiteDatabase database) { - database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `poll` TEXT"); - database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_poll` TEXT"); - } - }; - - public static final Migration MIGRATION_15_16 = new Migration(15, 16) { - @Override - public void migrate(@NonNull SupportSQLiteDatabase database) { - database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsPolls` INTEGER NOT NULL DEFAULT 1"); - } - }; - - public static final Migration MIGRATION_16_17 = new Migration(16, 17) { - @Override - public void migrate(@NonNull SupportSQLiteDatabase database) { - database.execSQL("ALTER TABLE `TimelineAccountEntity` ADD COLUMN `bot` INTEGER NOT NULL DEFAULT 0"); - } - }; - - public static final Migration MIGRATION_17_18 = new Migration(17, 18) { - @Override - public void migrate(@NonNull SupportSQLiteDatabase database) { - database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `alwaysOpenSpoiler` INTEGER NOT NULL DEFAULT 0"); - } - }; - - public static final Migration MIGRATION_18_19 = new Migration(18, 19) { - @Override - public void migrate(@NonNull SupportSQLiteDatabase database) { - database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `maxPollOptions` INTEGER"); - database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `maxPollOptionLength` INTEGER"); - - database.execSQL("ALTER TABLE `TootEntity` ADD COLUMN `poll` TEXT"); - } - }; - - public static final Migration MIGRATION_19_20 = new Migration(19, 20) { - @Override - public void migrate(@NonNull SupportSQLiteDatabase database) { - database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `bookmarked` INTEGER NOT NULL DEFAULT 0"); - database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_bookmarked` INTEGER NOT NULL DEFAULT 0"); - } - - }; - - public static final Migration MIGRATION_20_21 = new Migration(20, 21) { - @Override - public void migrate(@NonNull SupportSQLiteDatabase database) { - database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `version` TEXT"); - database.execSQL("ALTER TABLE `TootEntity` ADD COLUMN `markdownMode` INTEGER"); - } - }; - - public static final Migration MIGRATION_21_22 = new Migration(21, 22) { - @Override - public void migrate(@NonNull SupportSQLiteDatabase database) { - database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsEmojiReactions` INTEGER NOT NULL DEFAULT 1"); - } - }; - - public static final Migration MIGRATION_22_23 = new Migration(22, 23) { - @Override - public void migrate(@NonNull SupportSQLiteDatabase database) { - // leave markdownMode unused, we don't need it anymore but don't recreate table - // database.execSQL("ALTER TABLE `TootEntity` DROP COLUMN `markdownMode`"); - database.execSQL("ALTER TABLE `TootEntity` ADD COLUMN `formattingSyntax` TEXT NOT NULL DEFAULT ''"); - database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `defaultFormattingSyntax` TEXT NOT NULL DEFAULT ''"); - } - }; - - public static final Migration MIGRATION_23_24 = new Migration(23, 24) { - @Override - public void migrate(@NonNull SupportSQLiteDatabase database) { - database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsFollowRequested` INTEGER NOT NULL DEFAULT 1"); - } - }; - - public static final Migration MIGRATION_24_25 = new Migration(24, 25) { - @Override - public void migrate(@NonNull SupportSQLiteDatabase database) { - database.execSQL("CREATE TABLE `ChatEntity` (`localId` INTEGER NOT NULL," + - "`chatId` TEXT NOT NULL," + - "`accountId` TEXT NOT NULL," + - "`unread` INTEGER NOT NULL," + - "`updatedAt` INTEGER NOT NULL," + - "`lastMessageId` TEXT," + - "PRIMARY KEY (`localId`, `chatId`))"); - database.execSQL("CREATE TABLE `ChatMessageEntity` (`localId` INTEGER NOT NULL," + - "`messageId` TEXT NOT NULL," + - "`content` TEXT," + - "`chatId` TEXT NOT NULL," + - "`accountId` TEXT NOT NULL," + - "`createdAt` INTEGER NOT NULL," + - "`attachment` TEXT," + - "`emojis` TEXT NOT NULL," + - "PRIMARY KEY (`localId`, `messageId`))"); - database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `chatLimit` INTEGER"); - database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsChatMessages` INTEGER NOT NULL DEFAULT 1"); - database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsStreamingEnabled` INTEGER NOT NULL DEFAULT 1"); - database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `pleroma` TEXT"); - } - }; - - public static final Migration MIGRATION_25_26 = new Migration(25, 26) { - @Override - public void migrate(@NonNull SupportSQLiteDatabase database) { - database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsSubscriptions` INTEGER NOT NULL DEFAULT 1"); - database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsMove` INTEGER NOT NULL DEFAULT 1"); - } - }; - - public static final Migration MIGRATION_26_27 = new Migration(26, 27) { - @Override - public void migrate(@NonNull SupportSQLiteDatabase database) { - database.execSQL( - "CREATE TABLE IF NOT EXISTS `DraftEntity` (" + - "`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + - "`accountId` INTEGER NOT NULL, " + - "`inReplyToId` TEXT," + - "`content` TEXT," + - "`contentWarning` TEXT," + - "`sensitive` INTEGER NOT NULL," + - "`visibility` INTEGER NOT NULL," + - "`attachments` TEXT NOT NULL," + - "`poll` TEXT," + - "`formattingSyntax` TEXT NOT NULL," + - "`failedToSend` INTEGER NOT NULL)" - ); - } - }; - - public static final Migration MIGRATION_27_28 = new Migration(27, 28) { - @Override - public void migrate(@NonNull SupportSQLiteDatabase database) { - database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `editedAt` INTEGER"); - database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_editedAt` INTEGER"); - } - }; + public static final Migration MIGRATION_2_3 = + new Migration(2, 3) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL( + "CREATE TABLE TootEntity2 (uid INTEGER PRIMARY KEY AUTOINCREMENT NOT" + + " NULL, text TEXT, urls TEXT, contentWarning TEXT);"); + database.execSQL("INSERT INTO TootEntity2 SELECT * FROM TootEntity;"); + database.execSQL("DROP TABLE TootEntity;"); + database.execSQL("ALTER TABLE TootEntity2 RENAME TO TootEntity;"); + } + }; + + public static final Migration MIGRATION_3_4 = + new Migration(3, 4) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE TootEntity ADD COLUMN inReplyToId TEXT"); + database.execSQL("ALTER TABLE TootEntity ADD COLUMN inReplyToText TEXT"); + database.execSQL("ALTER TABLE TootEntity ADD COLUMN inReplyToUsername TEXT"); + database.execSQL("ALTER TABLE TootEntity ADD COLUMN visibility INTEGER"); + } + }; + + public static final Migration MIGRATION_4_5 = + new Migration(4, 5) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL( + "CREATE TABLE `AccountEntity` (" + + "`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, " + + "`isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, " + + "`username` TEXT NOT NULL, `displayName` TEXT NOT NULL, " + + "`profilePictureUrl` TEXT NOT NULL, " + + "`notificationsEnabled` INTEGER NOT NULL, " + + "`notificationsMentioned` INTEGER NOT NULL, " + + "`notificationsFollowed` INTEGER NOT NULL, " + + "`notificationsReblogged` INTEGER NOT NULL, " + + "`notificationsFavorited` INTEGER NOT NULL, " + + "`notificationSound` INTEGER NOT NULL, " + + "`notificationVibration` INTEGER NOT NULL, " + + "`notificationLight` INTEGER NOT NULL, " + + "`lastNotificationId` TEXT NOT NULL, " + + "`activeNotifications` TEXT NOT NULL)"); + database.execSQL( + "CREATE UNIQUE INDEX `index_AccountEntity_domain_accountId` ON" + + " `AccountEntity` (`domain`, `accountId`)"); + } + }; + + public static final Migration MIGRATION_5_6 = + new Migration(5, 6) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL( + "CREATE TABLE IF NOT EXISTS `EmojiListEntity` (`instance` TEXT NOT" + + " NULL, `emojiList` TEXT NOT NULL, PRIMARY KEY(`instance`))"); + } + }; + + public static final Migration MIGRATION_6_7 = + new Migration(6, 7) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL( + "CREATE TABLE IF NOT EXISTS `InstanceEntity` (`instance` TEXT NOT NULL," + + " `emojiList` TEXT, `maximumTootCharacters` INTEGER, PRIMARY" + + " KEY(`instance`))"); + database.execSQL( + "INSERT OR REPLACE INTO `InstanceEntity` SELECT `instance`,`emojiList`," + + " NULL FROM `EmojiListEntity`;"); + database.execSQL("DROP TABLE `EmojiListEntity`;"); + } + }; + + public static final Migration MIGRATION_7_8 = + new Migration(7, 8) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL( + "ALTER TABLE `AccountEntity` ADD COLUMN `emojis` TEXT NOT NULL DEFAULT" + + " '[]'"); + } + }; + + public static final Migration MIGRATION_8_9 = + new Migration(8, 9) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL( + "ALTER TABLE `TootEntity` ADD COLUMN `descriptions` TEXT DEFAULT '[]'"); + } + }; + + public static final Migration MIGRATION_9_10 = + new Migration(9, 10) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL( + "ALTER TABLE `AccountEntity` ADD COLUMN `defaultPostPrivacy` INTEGER" + + " NOT NULL DEFAULT 1"); + database.execSQL( + "ALTER TABLE `AccountEntity` ADD COLUMN `defaultMediaSensitivity`" + + " INTEGER NOT NULL DEFAULT 0"); + database.execSQL( + "ALTER TABLE `AccountEntity` ADD COLUMN `alwaysShowSensitiveMedia`" + + " INTEGER NOT NULL DEFAULT 0"); + database.execSQL( + "ALTER TABLE `AccountEntity` ADD COLUMN `mediaPreviewEnabled` INTEGER" + + " NOT NULL DEFAULT '1'"); + } + }; + + public static final Migration MIGRATION_10_11 = + new Migration(10, 11) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL( + "CREATE TABLE IF NOT EXISTS `TimelineAccountEntity` (" + + "`serverId` TEXT NOT NULL, " + + "`timelineUserId` INTEGER NOT NULL, " + + "`instance` TEXT NOT NULL, " + + "`localUsername` TEXT NOT NULL, " + + "`username` TEXT NOT NULL, " + + "`displayName` TEXT NOT NULL, " + + "`url` TEXT NOT NULL, " + + "`avatar` TEXT NOT NULL, " + + "`emojis` TEXT NOT NULL," + + "PRIMARY KEY(`serverId`, `timelineUserId`))"); + + database.execSQL( + "CREATE TABLE IF NOT EXISTS `TimelineStatusEntity` (`serverId` TEXT NOT" + + " NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL," + + " `authorServerId` TEXT,`instance` TEXT, `inReplyToId` TEXT," + + " `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER" + + " NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL," + + " `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT" + + " NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT" + + " NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments`" + + " TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId`" + + " TEXT, `reblogAccountId` TEXT, PRIMARY KEY(`serverId`," + + " `timelineUserId`), FOREIGN KEY(`authorServerId`," + + " `timelineUserId`) REFERENCES" + + " `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE" + + " NO ACTION ON DELETE NO ACTION )"); + database.execSQL( + "CREATE INDEX IF NOT" + + " EXISTS`index_TimelineStatusEntity_authorServerId_timelineUserId`" + + " ON `TimelineStatusEntity` (`authorServerId`," + + " `timelineUserId`)"); + } + }; + + public static final Migration MIGRATION_11_12 = + new Migration(11, 12) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + String defaultTabs = + TabDataKt.HOME + + ";" + + TabDataKt.NOTIFICATIONS + + ";" + + TabDataKt.LOCAL + + ";" + + TabDataKt.FEDERATED; + database.execSQL( + "ALTER TABLE `AccountEntity` ADD COLUMN `tabPreferences` TEXT NOT NULL" + + " DEFAULT '" + + defaultTabs + + "'"); + + database.execSQL( + "CREATE TABLE IF NOT EXISTS `ConversationEntity` (" + + "`accountId` INTEGER NOT NULL, " + + "`id` TEXT NOT NULL, " + + "`accounts` TEXT NOT NULL, " + + "`unread` INTEGER NOT NULL, " + + "`s_id` TEXT NOT NULL, " + + "`s_url` TEXT, " + + "`s_inReplyToId` TEXT, " + + "`s_inReplyToAccountId` TEXT, " + + "`s_account` TEXT NOT NULL, " + + "`s_content` TEXT NOT NULL, " + + "`s_createdAt` INTEGER NOT NULL, " + + "`s_emojis` TEXT NOT NULL, " + + "`s_favouritesCount` INTEGER NOT NULL, " + + "`s_favourited` INTEGER NOT NULL, " + + "`s_sensitive` INTEGER NOT NULL, " + + "`s_spoilerText` TEXT NOT NULL, " + + "`s_attachments` TEXT NOT NULL, " + + "`s_mentions` TEXT NOT NULL, " + + "`s_showingHiddenContent` INTEGER NOT NULL, " + + "`s_expanded` INTEGER NOT NULL, " + + "`s_collapsible` INTEGER NOT NULL, " + + "`s_collapsed` INTEGER NOT NULL, " + + "PRIMARY KEY(`id`, `accountId`))"); + } + }; + + public static final Migration MIGRATION_12_13 = + new Migration(12, 13) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + + database.execSQL("DROP TABLE IF EXISTS `TimelineAccountEntity`"); + database.execSQL("DROP TABLE IF EXISTS `TimelineStatusEntity`"); + + database.execSQL( + "CREATE TABLE IF NOT EXISTS `TimelineAccountEntity` (" + + "`serverId` TEXT NOT NULL, " + + "`timelineUserId` INTEGER NOT NULL, " + + "`localUsername` TEXT NOT NULL, " + + "`username` TEXT NOT NULL, " + + "`displayName` TEXT NOT NULL, " + + "`url` TEXT NOT NULL, " + + "`avatar` TEXT NOT NULL, " + + "`emojis` TEXT NOT NULL," + + "PRIMARY KEY(`serverId`, `timelineUserId`))"); + + database.execSQL( + "CREATE TABLE IF NOT EXISTS `TimelineStatusEntity` (`serverId` TEXT NOT" + + " NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL," + + " `authorServerId` TEXT,`inReplyToId` TEXT, `inReplyToAccountId`" + + " TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis`" + + " TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount`" + + " INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `favourited`" + + " INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText`" + + " TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions`" + + " TEXT, `application` TEXT, `reblogServerId` TEXT," + + " `reblogAccountId` TEXT, PRIMARY KEY(`serverId`," + + " `timelineUserId`), FOREIGN KEY(`authorServerId`," + + " `timelineUserId`) REFERENCES" + + " `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE" + + " NO ACTION ON DELETE NO ACTION )"); + database.execSQL( + "CREATE INDEX IF NOT" + + " EXISTS`index_TimelineStatusEntity_authorServerId_timelineUserId`" + + " ON `TimelineStatusEntity` (`authorServerId`," + + " `timelineUserId`)"); + } + }; + + public static final Migration MIGRATION_10_13 = + new Migration(10, 13) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + MIGRATION_11_12.migrate(database); + MIGRATION_12_13.migrate(database); + } + }; + + public static final Migration MIGRATION_13_14 = + new Migration(13, 14) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL( + "ALTER TABLE `AccountEntity` ADD COLUMN `notificationsFilter` TEXT NOT" + + " NULL DEFAULT '[]'"); + } + }; + + public static final Migration MIGRATION_14_15 = + new Migration(14, 15) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `poll` TEXT"); + database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_poll` TEXT"); + } + }; + + public static final Migration MIGRATION_15_16 = + new Migration(15, 16) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL( + "ALTER TABLE `AccountEntity` ADD COLUMN `notificationsPolls` INTEGER" + + " NOT NULL DEFAULT 1"); + } + }; + + public static final Migration MIGRATION_16_17 = + new Migration(16, 17) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL( + "ALTER TABLE `TimelineAccountEntity` ADD COLUMN `bot` INTEGER NOT NULL" + + " DEFAULT 0"); + } + }; + + public static final Migration MIGRATION_17_18 = + new Migration(17, 18) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL( + "ALTER TABLE `AccountEntity` ADD COLUMN `alwaysOpenSpoiler` INTEGER NOT" + + " NULL DEFAULT 0"); + } + }; + + public static final Migration MIGRATION_18_19 = + new Migration(18, 19) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL( + "ALTER TABLE `InstanceEntity` ADD COLUMN `maxPollOptions` INTEGER"); + database.execSQL( + "ALTER TABLE `InstanceEntity` ADD COLUMN `maxPollOptionLength`" + + " INTEGER"); + + database.execSQL("ALTER TABLE `TootEntity` ADD COLUMN `poll` TEXT"); + } + }; + + public static final Migration MIGRATION_19_20 = + new Migration(19, 20) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL( + "ALTER TABLE `TimelineStatusEntity` ADD COLUMN `bookmarked` INTEGER NOT" + + " NULL DEFAULT 0"); + database.execSQL( + "ALTER TABLE `ConversationEntity` ADD COLUMN `s_bookmarked` INTEGER NOT" + + " NULL DEFAULT 0"); + } + }; + + public static final Migration MIGRATION_20_21 = + new Migration(20, 21) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `version` TEXT"); + database.execSQL("ALTER TABLE `TootEntity` ADD COLUMN `markdownMode` INTEGER"); + } + }; + + public static final Migration MIGRATION_21_22 = + new Migration(21, 22) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL( + "ALTER TABLE `AccountEntity` ADD COLUMN `notificationsEmojiReactions`" + + " INTEGER NOT NULL DEFAULT 1"); + } + }; + + public static final Migration MIGRATION_22_23 = + new Migration(22, 23) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + // leave markdownMode unused, we don't need it anymore but don't recreate table + // database.execSQL("ALTER TABLE `TootEntity` DROP COLUMN `markdownMode`"); + database.execSQL( + "ALTER TABLE `TootEntity` ADD COLUMN `formattingSyntax` TEXT NOT NULL" + + " DEFAULT ''"); + database.execSQL( + "ALTER TABLE `AccountEntity` ADD COLUMN `defaultFormattingSyntax` TEXT" + + " NOT NULL DEFAULT ''"); + } + }; + + public static final Migration MIGRATION_23_24 = + new Migration(23, 24) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL( + "ALTER TABLE `AccountEntity` ADD COLUMN `notificationsFollowRequested`" + + " INTEGER NOT NULL DEFAULT 1"); + } + }; + + public static final Migration MIGRATION_24_25 = + new Migration(24, 25) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL( + "CREATE TABLE `ChatEntity` (`localId` INTEGER NOT NULL," + + "`chatId` TEXT NOT NULL," + + "`accountId` TEXT NOT NULL," + + "`unread` INTEGER NOT NULL," + + "`updatedAt` INTEGER NOT NULL," + + "`lastMessageId` TEXT," + + "PRIMARY KEY (`localId`, `chatId`))"); + database.execSQL( + "CREATE TABLE `ChatMessageEntity` (`localId` INTEGER NOT NULL," + + "`messageId` TEXT NOT NULL," + + "`content` TEXT," + + "`chatId` TEXT NOT NULL," + + "`accountId` TEXT NOT NULL," + + "`createdAt` INTEGER NOT NULL," + + "`attachment` TEXT," + + "`emojis` TEXT NOT NULL," + + "PRIMARY KEY (`localId`, `messageId`))"); + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `chatLimit` INTEGER"); + database.execSQL( + "ALTER TABLE `AccountEntity` ADD COLUMN `notificationsChatMessages`" + + " INTEGER NOT NULL DEFAULT 1"); + database.execSQL( + "ALTER TABLE `AccountEntity` ADD COLUMN `notificationsStreamingEnabled`" + + " INTEGER NOT NULL DEFAULT 1"); + database.execSQL( + "ALTER TABLE `TimelineStatusEntity` ADD COLUMN `pleroma` TEXT"); + } + }; + + public static final Migration MIGRATION_25_26 = + new Migration(25, 26) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL( + "ALTER TABLE `AccountEntity` ADD COLUMN `notificationsSubscriptions`" + + " INTEGER NOT NULL DEFAULT 1"); + database.execSQL( + "ALTER TABLE `AccountEntity` ADD COLUMN `notificationsMove` INTEGER NOT" + + " NULL DEFAULT 1"); + } + }; + + public static final Migration MIGRATION_26_27 = + new Migration(26, 27) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL( + "CREATE TABLE IF NOT EXISTS `DraftEntity` (" + + "`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`accountId` INTEGER NOT NULL, " + + "`inReplyToId` TEXT," + + "`content` TEXT," + + "`contentWarning` TEXT," + + "`sensitive` INTEGER NOT NULL," + + "`visibility` INTEGER NOT NULL," + + "`attachments` TEXT NOT NULL," + + "`poll` TEXT," + + "`formattingSyntax` TEXT NOT NULL," + + "`failedToSend` INTEGER NOT NULL)"); + } + }; + + public static final Migration MIGRATION_27_28 = + new Migration(27, 28) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL( + "ALTER TABLE `TimelineStatusEntity` ADD COLUMN `editedAt` INTEGER"); + database.execSQL( + "ALTER TABLE `ConversationEntity` ADD COLUMN `s_editedAt` INTEGER"); + } + }; } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/db/TootDao.java b/husky/app/src/main/java/com/keylesspalace/tusky/db/TootDao.java index f46c275..652a2a5 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/db/TootDao.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/db/TootDao.java @@ -17,17 +17,14 @@ package com.keylesspalace.tusky.db; import androidx.room.Dao; import androidx.room.Query; - -import java.util.List; - import io.reactivex.Observable; +import java.util.List; /** * Created by cto3543 on 28/06/2017. * - * DAO to fetch and update toots in the DB. + * <p>DAO to fetch and update toots in the DB. */ - @Dao public interface TootDao { @@ -42,4 +39,4 @@ public interface TootDao { @Query("SELECT COUNT(*) FROM TootEntity") Observable<Integer> savedTootCount(); -}
\ No newline at end of file +} diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/db/TootEntity.java b/husky/app/src/main/java/com/keylesspalace/tusky/db/TootEntity.java index d40bcc7..aea0096 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/db/TootEntity.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/db/TootEntity.java @@ -15,21 +15,17 @@ package com.keylesspalace.tusky.db; -import com.google.gson.Gson; -import com.keylesspalace.tusky.entity.NewPoll; -import com.keylesspalace.tusky.entity.Status; - import androidx.annotation.*; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.PrimaryKey; import androidx.room.TypeConverter; import androidx.room.TypeConverters; +import com.google.gson.Gson; +import com.keylesspalace.tusky.entity.NewPoll; +import com.keylesspalace.tusky.entity.Status; -/** - * Toot model. - */ - +/** Toot model. */ @Entity @TypeConverters(TootEntity.Converters.class) public class TootEntity { @@ -65,19 +61,28 @@ public class TootEntity { @Nullable @ColumnInfo(name = "poll") private final NewPoll poll; - + @NonNull @ColumnInfo(name = "formattingSyntax") private final String formattingSyntax; - + /* DEPRECATED */ @Nullable @ColumnInfo(name = "markdownMode") public Boolean markdownMode = false; - public TootEntity(int uid, String text, String urls, String descriptions, String contentWarning, String inReplyToId, - @Nullable String inReplyToText, @Nullable String inReplyToUsername, - Status.Visibility visibility, @Nullable NewPoll poll, String formattingSyntax) { + public TootEntity( + int uid, + String text, + String urls, + String descriptions, + String contentWarning, + String inReplyToId, + @Nullable String inReplyToText, + @Nullable String inReplyToUsername, + Status.Visibility visibility, + @Nullable NewPoll poll, + String formattingSyntax) { this.uid = uid; this.text = text; this.urls = urls; @@ -133,11 +138,11 @@ public class TootEntity { public NewPoll getPoll() { return poll; } - + public String getFormattingSyntax() { return formattingSyntax; } - + @Nullable public Boolean getMarkdownMode() { return markdownMode; diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/fragment/BaseFragment.java b/husky/app/src/main/java/com/keylesspalace/tusky/fragment/BaseFragment.java index b674b8b..7134c2e 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/fragment/BaseFragment.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/fragment/BaseFragment.java @@ -18,10 +18,8 @@ package com.keylesspalace.tusky.fragment; import android.os.Bundle; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; - import java.util.ArrayList; import java.util.List; - import retrofit2.Call; public class BaseFragment extends Fragment { diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/husky/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java index 59da514..3b3beee 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -20,6 +20,11 @@ package com.keylesspalace.tusky.fragment; +import static com.keylesspalace.tusky.util.StringUtils.isLessThan; +import static com.uber.autodispose.AutoDispose.autoDisposable; +import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from; +import static org.koin.java.KoinJavaComponent.inject; + import android.app.Activity; import android.content.Context; import android.content.DialogInterface; @@ -86,14 +91,11 @@ import com.keylesspalace.tusky.util.ListUtils; import com.keylesspalace.tusky.util.NotificationTypeConverterKt; import com.keylesspalace.tusky.util.PairedList; import com.keylesspalace.tusky.util.StatusDisplayOptions; -import static com.keylesspalace.tusky.util.StringUtils.isLessThan; import com.keylesspalace.tusky.util.ViewDataUtils; import com.keylesspalace.tusky.view.BackgroundMessageView; import com.keylesspalace.tusky.view.EndlessOnScrollListener; import com.keylesspalace.tusky.viewdata.NotificationViewData; import com.keylesspalace.tusky.viewdata.StatusViewData; -import static com.uber.autodispose.AutoDispose.autoDisposable; -import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from; import io.reactivex.Observable; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -110,31 +112,33 @@ import kotlin.Unit; import kotlin.collections.CollectionsKt; import kotlin.jvm.functions.Function1; import okhttp3.ResponseBody; -import static org.koin.java.KoinJavaComponent.inject; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; public class NotificationsFragment extends SFragment - implements SwipeRefreshLayout.OnRefreshListener, StatusActionListener, - NotificationsAdapter.NotificationActionListener, AccountActionListener, ReselectableFragment -{ + implements SwipeRefreshLayout.OnRefreshListener, + StatusActionListener, + NotificationsAdapter.NotificationActionListener, + AccountActionListener, + ReselectableFragment { private static final String TAG = "NotificationF"; // logging tag private static final int LOAD_AT_ONCE = 30; private int maxPlaceholderId = 0; - private Set<Notification.Type> notificationFilter = new HashSet<>(); private enum FetchEnd { - TOP, BOTTOM, MIDDLE + TOP, + BOTTOM, + MIDDLE } /** - * Placeholder for the notificationsEnabled. Consider moving to the separate class to hide constructor - * and reuse in different places as needed. + * Placeholder for the notificationsEnabled. Consider moving to the separate class to hide + * constructor and reuse in different places as needed. */ private static final class Placeholder { final long id; @@ -171,22 +175,32 @@ public class NotificationsFragment extends SFragment // Each element is either a Notification for loading data or a Placeholder private final PairedList<Either<Placeholder, Notification>, NotificationViewData> - notifications = - new PairedList<>(new Function<Either<Placeholder, Notification>, NotificationViewData>() { - @Override - public NotificationViewData apply(Either<Placeholder, Notification> input) { - if(input.isRight()) { - Notification notification = - Notification.rewriteToStatusTypeIfNeeded(input.asRight(), - accountManager.getValue().getActiveAccount().getAccountId()); - - return ViewDataUtils.notificationToViewData(notification, - alwaysShowSensitiveMedia, alwaysOpenSpoiler); - } else { - return new NotificationViewData.Placeholder(input.asLeft().id, false); - } - } - }); + notifications = + new PairedList<>( + new Function< + Either<Placeholder, Notification>, NotificationViewData>() { + @Override + public NotificationViewData apply( + Either<Placeholder, Notification> input) { + if (input.isRight()) { + Notification notification = + Notification.rewriteToStatusTypeIfNeeded( + input.asRight(), + accountManager + .getValue() + .getActiveAccount() + .getAccountId()); + + return ViewDataUtils.notificationToViewData( + notification, + alwaysShowSensitiveMedia, + alwaysOpenSpoiler); + } else { + return new NotificationViewData.Placeholder( + input.asLeft().id, false); + } + } + }); public static NotificationsFragment newInstance() { NotificationsFragment fragment = new NotificationsFragment(); @@ -197,19 +211,20 @@ public class NotificationsFragment extends SFragment @Nullable @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) - { + public View onCreateView( + @NonNull LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { View rootView = - inflater.inflate(R.layout.fragment_timeline_notifications, container, false); + inflater.inflate(R.layout.fragment_timeline_notifications, container, false); @NonNull Context context = inflater.getContext(); // from inflater to silence warning SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); boolean showNotificationsFilterSetting = - preferences.getBoolean("showNotificationsFilter", true); - //Clear notifications on filter visibility change to force refresh - if(showNotificationsFilterSetting != showNotificationsFilter) { + preferences.getBoolean("showNotificationsFilter", true); + // Clear notifications on filter visibility change to force refresh + if (showNotificationsFilterSetting != showNotificationsFilter) { notifications.clear(); } showNotificationsFilter = showNotificationsFilterSetting; @@ -231,35 +246,47 @@ public class NotificationsFragment extends SFragment layoutManager = new LinearLayoutManager(context); recyclerView.setLayoutManager(layoutManager); recyclerView.setAccessibilityDelegateCompat( - new ListStatusAccessibilityDelegate(recyclerView, this, (pos) -> { - NotificationViewData notification = notifications.getPairedItemOrNull(pos); - // We support replies only for now - if(notification instanceof NotificationViewData.Concrete) { - return ((NotificationViewData.Concrete) notification).getStatusViewData(); - } else { - return null; - } - })); + new ListStatusAccessibilityDelegate( + recyclerView, + this, + (pos) -> { + NotificationViewData notification = + notifications.getPairedItemOrNull(pos); + // We support replies only for now + if (notification instanceof NotificationViewData.Concrete) { + return ((NotificationViewData.Concrete) notification) + .getStatusViewData(); + } else { + return null; + } + })); recyclerView.addItemDecoration( - new DividerItemDecoration(context, DividerItemDecoration.VERTICAL)); + new DividerItemDecoration(context, DividerItemDecoration.VERTICAL)); StatusDisplayOptions statusDisplayOptions = - new StatusDisplayOptions(preferences.getBoolean("animateGifAvatars", false), - accountManager.getValue().getActiveAccount().getMediaPreviewEnabled(), - preferences.getBoolean("absoluteTimeView", false), - preferences.getBoolean("showBotOverlay", true), - preferences.getBoolean("useBlurhash", true), CardViewMode.NONE, - preferences.getBoolean("confirmReblogs", true), - preferences.getBoolean(PrefKeys.RENDER_STATUS_AS_MENTION, true), - preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false)); + new StatusDisplayOptions( + preferences.getBoolean("animateGifAvatars", false), + accountManager.getValue().getActiveAccount().getMediaPreviewEnabled(), + preferences.getBoolean("absoluteTimeView", false), + preferences.getBoolean("showBotOverlay", true), + preferences.getBoolean("useBlurhash", true), + CardViewMode.NONE, + preferences.getBoolean("confirmReblogs", true), + preferences.getBoolean(PrefKeys.RENDER_STATUS_AS_MENTION, true), + preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false)); withMuted = !preferences.getBoolean(PrefKeys.HIDE_MUTED_USERS, false); adapter = - new NotificationsAdapter(accountManager.getValue().getActiveAccount().getAccountId(), - dataSource, statusDisplayOptions, this, this, this); + new NotificationsAdapter( + accountManager.getValue().getActiveAccount().getAccountId(), + dataSource, + statusDisplayOptions, + this, + this, + this); alwaysShowSensitiveMedia = - accountManager.getValue().getActiveAccount().getAlwaysShowSensitiveMedia(); + accountManager.getValue().getActiveAccount().getAlwaysShowSensitiveMedia(); alwaysOpenSpoiler = accountManager.getValue().getActiveAccount().getAlwaysOpenSpoiler(); recyclerView.setAdapter(adapter); @@ -274,7 +301,7 @@ public class NotificationsFragment extends SFragment buttonFilter = rootView.findViewById(R.id.buttonFilter); buttonFilter.setOnClickListener(v -> showFilterMenu()); - if(notifications.isEmpty()) { + if (notifications.isEmpty()) { swipeRefreshLayout.setEnabled(false); sendFetchNotificationsRequest(null, null, FetchEnd.BOTTOM, -1); } else { @@ -290,82 +317,93 @@ public class NotificationsFragment extends SFragment private void updateFilterVisibility() { CoordinatorLayout.LayoutParams params = - (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams(); - if(showNotificationsFilter && !showingError) { + (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams(); + if (showNotificationsFilter && !showingError) { appBarOptions.setExpanded(true, false); appBarOptions.setVisibility(View.VISIBLE); - //Set content behaviour to hide filter on scroll + // Set content behaviour to hide filter on scroll params.setBehavior(new AppBarLayout.ScrollingViewBehavior()); } else { appBarOptions.setExpanded(false, false); appBarOptions.setVisibility(View.GONE); - //Clear behaviour to hide app bar + // Clear behaviour to hide app bar params.setBehavior(null); } } private void confirmClearNotifications() { - new AlertDialog.Builder(getContext()).setMessage(R.string.notification_clear_text) - .setPositiveButton(android.R.string.yes, - (DialogInterface dia, int which) -> clearNotifications()) - .setNegativeButton(android.R.string.no, null).show(); + new AlertDialog.Builder(getContext()) + .setMessage(R.string.notification_clear_text) + .setPositiveButton( + android.R.string.yes, + (DialogInterface dia, int which) -> clearNotifications()) + .setNegativeButton(android.R.string.no, null) + .show(); } private void handleFavEvent(FavoriteEvent event) { Pair<Integer, Notification> posAndNotification = findReplyPosition(event.getStatusId()); - if(posAndNotification == null) { + if (posAndNotification == null) { return; } //noinspection ConstantConditions - setFavouriteForStatus(posAndNotification.first, posAndNotification.second.getStatus(), - event.getFavourite()); + setFavouriteForStatus( + posAndNotification.first, + posAndNotification.second.getStatus(), + event.getFavourite()); } private void handleBookmarkEvent(BookmarkEvent event) { Pair<Integer, Notification> posAndNotification = findReplyPosition(event.getStatusId()); - if(posAndNotification == null) { + if (posAndNotification == null) { return; } //noinspection ConstantConditions - setBookmarkForStatus(posAndNotification.first, posAndNotification.second.getStatus(), - event.getBookmark()); + setBookmarkForStatus( + posAndNotification.first, + posAndNotification.second.getStatus(), + event.getBookmark()); } private void handleReblogEvent(ReblogEvent event) { Pair<Integer, Notification> posAndNotification = findReplyPosition(event.getStatusId()); - if(posAndNotification == null) { + if (posAndNotification == null) { return; } //noinspection ConstantConditions - setReblogForStatus(posAndNotification.first, posAndNotification.second.getStatus(), - event.getReblog()); + setReblogForStatus( + posAndNotification.first, posAndNotification.second.getStatus(), event.getReblog()); } private void handleMuteStatusEvent(MuteConversationEvent event) { Pair<Integer, Notification> posAndNotification = findReplyPosition(event.getStatusId()); - if(posAndNotification == null) { + if (posAndNotification == null) { return; } String conversationId = posAndNotification.second.getStatus().getConversationId(); - if(conversationId.isEmpty()) { // invalid conversation ID - if(withMuted) { - setMutedStatusForStatus(posAndNotification.first, - posAndNotification.second.getStatus(), event.getMute(), event.getMute()); + if (conversationId.isEmpty()) { // invalid conversation ID + if (withMuted) { + setMutedStatusForStatus( + posAndNotification.first, + posAndNotification.second.getStatus(), + event.getMute(), + event.getMute()); } else { notifications.remove(posAndNotification.first); } } else { //noinspection ConstantConditions - if(withMuted) { - for(int i = 0; i < notifications.size(); i++) { + if (withMuted) { + for (int i = 0; i < notifications.size(); i++) { Notification notification = notifications.get(i).asRightOrNull(); - if(notification != null && notification.getStatus() != null && - notification.getType() == Notification.Type.MENTION && - notification.getStatus().getConversationId() == conversationId) { - setMutedStatusForStatus(i, notification.getStatus(), event.getMute(), - event.getMute()); + if (notification != null + && notification.getStatus() != null + && notification.getType() == Notification.Type.MENTION + && notification.getStatus().getConversationId() == conversationId) { + setMutedStatusForStatus( + i, notification.getStatus(), event.getMute(), event.getMute()); } } } else { @@ -379,13 +417,14 @@ public class NotificationsFragment extends SFragment String id = event.getAccountId(); boolean mute = event.getMute(); - if(withMuted) { - for(int i = 0; i < notifications.size(); i++) { + if (withMuted) { + for (int i = 0; i < notifications.size(); i++) { Notification notification = notifications.get(i).asRightOrNull(); - if(notification != null && notification.getStatus() != null && - notification.getType() == Notification.Type.MENTION && - notification.getAccount().getId().equals(id) && - !notification.getStatus().isThreadMuted()) { + if (notification != null + && notification.getStatus() != null + && notification.getType() == Notification.Type.MENTION + && notification.getAccount().getId().equals(id) + && !notification.getStatus().isThreadMuted()) { setMutedStatusForStatus(i, notification.getStatus(), mute, false); } } @@ -399,7 +438,7 @@ public class NotificationsFragment extends SFragment public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); Activity activity = getActivity(); - if(activity == null) { + if (activity == null) { throw new AssertionError("Activity is null"); } @@ -409,55 +448,61 @@ public class NotificationsFragment extends SFragment * the compose button on down-scroll. */ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); hideFab = preferences.getBoolean("fabHide", false); - scrollListener = new EndlessOnScrollListener(layoutManager) { - @Override - public void onScrolled(@NonNull RecyclerView view, int dx, int dy) { - super.onScrolled(view, dx, dy); - - ActionButtonActivity activity = (ActionButtonActivity) getActivity(); - FloatingActionButton composeButton = activity.getActionButton(); - - if(composeButton != null) { - if(hideFab) { - if(dy > 0 && composeButton.isShown()) { - composeButton.hide(); // hides the button if we're scrolling down - } else if(dy < 0 && !composeButton.isShown()) { - composeButton.show(); // shows it if we are scrolling up + scrollListener = + new EndlessOnScrollListener(layoutManager) { + @Override + public void onScrolled(@NonNull RecyclerView view, int dx, int dy) { + super.onScrolled(view, dx, dy); + + ActionButtonActivity activity = (ActionButtonActivity) getActivity(); + FloatingActionButton composeButton = activity.getActionButton(); + + if (composeButton != null) { + if (hideFab) { + if (dy > 0 && composeButton.isShown()) { + composeButton + .hide(); // hides the button if we're scrolling down + } else if (dy < 0 && !composeButton.isShown()) { + composeButton.show(); // shows it if we are scrolling up + } + } else if (!composeButton.isShown()) { + composeButton.show(); + } } - } else if(!composeButton.isShown()) { - composeButton.show(); } - } - } - @Override - public void onLoadMore(int totalItemsCount, RecyclerView view) { - NotificationsFragment.this.onLoadMore(); - } - }; + @Override + public void onLoadMore(int totalItemsCount, RecyclerView view) { + NotificationsFragment.this.onLoadMore(); + } + }; recyclerView.addOnScrollListener(scrollListener); - eventHub.getEvents().observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))).subscribe(event -> { - if(event instanceof FavoriteEvent) { - handleFavEvent((FavoriteEvent) event); - } else if(event instanceof BookmarkEvent) { - handleBookmarkEvent((BookmarkEvent) event); - } else if(event instanceof ReblogEvent) { - handleReblogEvent((ReblogEvent) event); - } else if(event instanceof MuteConversationEvent) { - handleMuteStatusEvent((MuteConversationEvent) event); - } else if(event instanceof BlockEvent) { - removeAllByAccountId(((BlockEvent) event).getAccountId()); - } else if(event instanceof MuteEvent) { - handleMuteEvent((MuteEvent) event); - } else if(event instanceof PreferenceChangedEvent) { - onPreferenceChanged(((PreferenceChangedEvent) event).getPreferenceKey()); - } else if(event instanceof EmojiReactEvent) { - handleEmojiReactEvent((EmojiReactEvent) event); - } - }); + eventHub.getEvents() + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + event -> { + if (event instanceof FavoriteEvent) { + handleFavEvent((FavoriteEvent) event); + } else if (event instanceof BookmarkEvent) { + handleBookmarkEvent((BookmarkEvent) event); + } else if (event instanceof ReblogEvent) { + handleReblogEvent((ReblogEvent) event); + } else if (event instanceof MuteConversationEvent) { + handleMuteStatusEvent((MuteConversationEvent) event); + } else if (event instanceof BlockEvent) { + removeAllByAccountId(((BlockEvent) event).getAccountId()); + } else if (event instanceof MuteEvent) { + handleMuteEvent((MuteEvent) event); + } else if (event instanceof PreferenceChangedEvent) { + onPreferenceChanged( + ((PreferenceChangedEvent) event).getPreferenceKey()); + } else if (event instanceof EmojiReactEvent) { + handleEmojiReactEvent((EmojiReactEvent) event); + } + }); } @Override @@ -466,7 +511,7 @@ public class NotificationsFragment extends SFragment this.showingError = false; Either<Placeholder, Notification> first = CollectionsKt.firstOrNull(this.notifications); String topId; - if(first != null && first.isRight()) { + if (first != null && first.isRight()) { topId = first.asRight().getId(); } else { topId = null; @@ -484,31 +529,43 @@ public class NotificationsFragment extends SFragment final Notification notification = notifications.get(position).asRight(); final Status status = notification.getStatus(); Objects.requireNonNull(status, "Reblog on notification without status"); - timelineCases.getValue().reblog(status, reblog).observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this))) - .subscribe((newStatus) -> setReblogForStatus(position, status, reblog), - (t) -> Log.d(getClass().getSimpleName(), - "Failed to reblog status: " + status.getId(), t)); + timelineCases + .getValue() + .reblog(status, reblog) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this))) + .subscribe( + (newStatus) -> setReblogForStatus(position, status, reblog), + (t) -> + Log.d( + getClass().getSimpleName(), + "Failed to reblog status: " + status.getId(), + t)); } private void setReblogForStatus(int position, Status status, boolean reblog) { status.setReblogged(reblog); - if(status.getReblog() != null) { + if (status.getReblog() != null) { status.getReblog().setReblogged(reblog); } NotificationViewData.Concrete viewdata = - (NotificationViewData.Concrete) notifications.getPairedItem(position); + (NotificationViewData.Concrete) notifications.getPairedItem(position); StatusViewData.Builder viewDataBuilder = - new StatusViewData.Builder(viewdata.getStatusViewData()); + new StatusViewData.Builder(viewdata.getStatusViewData()); viewDataBuilder.setReblogged(reblog); NotificationViewData.Concrete newViewData = - new NotificationViewData.Concrete(viewdata.getType(), viewdata.getId(), - viewdata.getAccount(), viewDataBuilder.createStatusViewData(), viewdata.getEmoji(), - viewdata.getEmojiUrl(), viewdata.getTarget()); + new NotificationViewData.Concrete( + viewdata.getType(), + viewdata.getId(), + viewdata.getAccount(), + viewDataBuilder.createStatusViewData(), + viewdata.getEmoji(), + viewdata.getEmojiUrl(), + viewdata.getTarget()); notifications.setPairedItem(position, newViewData); updateAdapter(); } @@ -518,31 +575,43 @@ public class NotificationsFragment extends SFragment final Notification notification = notifications.get(position).asRight(); final Status status = notification.getStatus(); - timelineCases.getValue().favourite(status, favourite) - .observeOn(AndroidSchedulers.mainThread()).as(autoDisposable(from(this))) - .subscribe((newStatus) -> setFavouriteForStatus(position, status, favourite), - (t) -> Log.d(getClass().getSimpleName(), - "Failed to favourite status: " + status.getId(), t)); + timelineCases + .getValue() + .favourite(status, favourite) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this))) + .subscribe( + (newStatus) -> setFavouriteForStatus(position, status, favourite), + (t) -> + Log.d( + getClass().getSimpleName(), + "Failed to favourite status: " + status.getId(), + t)); } private void setFavouriteForStatus(int position, Status status, boolean favourite) { status.setFavourited(favourite); - if(status.getReblog() != null) { + if (status.getReblog() != null) { status.getReblog().setFavourited(favourite); } NotificationViewData.Concrete viewdata = - (NotificationViewData.Concrete) notifications.getPairedItem(position); + (NotificationViewData.Concrete) notifications.getPairedItem(position); StatusViewData.Builder viewDataBuilder = - new StatusViewData.Builder(viewdata.getStatusViewData()); + new StatusViewData.Builder(viewdata.getStatusViewData()); viewDataBuilder.setFavourited(favourite); NotificationViewData.Concrete newViewData = - new NotificationViewData.Concrete(viewdata.getType(), viewdata.getId(), - viewdata.getAccount(), viewDataBuilder.createStatusViewData(), viewdata.getEmoji(), - viewdata.getEmojiUrl(), viewdata.getTarget()); + new NotificationViewData.Concrete( + viewdata.getType(), + viewdata.getId(), + viewdata.getAccount(), + viewDataBuilder.createStatusViewData(), + viewdata.getEmoji(), + viewdata.getEmojiUrl(), + viewdata.getTarget()); notifications.setPairedItem(position, newViewData); updateAdapter(); @@ -553,31 +622,43 @@ public class NotificationsFragment extends SFragment final Notification notification = notifications.get(position).asRight(); final Status status = notification.getStatus(); - timelineCases.getValue().bookmark(status, bookmark) - .observeOn(AndroidSchedulers.mainThread()).as(autoDisposable(from(this))) - .subscribe((newStatus) -> setBookmarkForStatus(position, status, bookmark), - (t) -> Log.d(getClass().getSimpleName(), - "Failed to bookmark status: " + status.getId(), t)); + timelineCases + .getValue() + .bookmark(status, bookmark) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this))) + .subscribe( + (newStatus) -> setBookmarkForStatus(position, status, bookmark), + (t) -> + Log.d( + getClass().getSimpleName(), + "Failed to bookmark status: " + status.getId(), + t)); } private void setBookmarkForStatus(int position, Status status, boolean bookmark) { status.setBookmarked(bookmark); - if(status.getReblog() != null) { + if (status.getReblog() != null) { status.getReblog().setBookmarked(bookmark); } NotificationViewData.Concrete viewdata = - (NotificationViewData.Concrete) notifications.getPairedItem(position); + (NotificationViewData.Concrete) notifications.getPairedItem(position); StatusViewData.Builder viewDataBuilder = - new StatusViewData.Builder(viewdata.getStatusViewData()); + new StatusViewData.Builder(viewdata.getStatusViewData()); viewDataBuilder.setBookmarked(bookmark); NotificationViewData.Concrete newViewData = - new NotificationViewData.Concrete(viewdata.getType(), viewdata.getId(), - viewdata.getAccount(), viewDataBuilder.createStatusViewData(), viewdata.getEmoji(), - viewdata.getEmojiUrl(), viewdata.getTarget()); + new NotificationViewData.Concrete( + viewdata.getType(), + viewdata.getId(), + viewdata.getAccount(), + viewDataBuilder.createStatusViewData(), + viewdata.getEmoji(), + viewdata.getEmojiUrl(), + viewdata.getTarget()); notifications.setPairedItem(position, newViewData); updateAdapter(); @@ -587,25 +668,34 @@ public class NotificationsFragment extends SFragment final Notification notification = notifications.get(position).asRight(); final Status status = notification.getStatus(); - timelineCases.getValue().voteInPoll(status, choices) - .observeOn(AndroidSchedulers.mainThread()).as(autoDisposable(from(this))) - .subscribe((newPoll) -> setVoteForPoll(position, newPoll), - (t) -> Log.d(TAG, "Failed to vote in poll: " + status.getId(), t)); + timelineCases + .getValue() + .voteInPoll(status, choices) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this))) + .subscribe( + (newPoll) -> setVoteForPoll(position, newPoll), + (t) -> Log.d(TAG, "Failed to vote in poll: " + status.getId(), t)); } private void setVoteForPoll(int position, Poll poll) { NotificationViewData.Concrete viewdata = - (NotificationViewData.Concrete) notifications.getPairedItem(position); + (NotificationViewData.Concrete) notifications.getPairedItem(position); StatusViewData.Builder viewDataBuilder = - new StatusViewData.Builder(viewdata.getStatusViewData()); + new StatusViewData.Builder(viewdata.getStatusViewData()); viewDataBuilder.setPoll(poll); NotificationViewData.Concrete newViewData = - new NotificationViewData.Concrete(viewdata.getType(), viewdata.getId(), - viewdata.getAccount(), viewDataBuilder.createStatusViewData(), viewdata.getEmoji(), - viewdata.getEmojiUrl(), viewdata.getTarget()); + new NotificationViewData.Concrete( + viewdata.getType(), + viewdata.getId(), + viewdata.getAccount(), + viewDataBuilder.createStatusViewData(), + viewdata.getEmoji(), + viewdata.getEmojiUrl(), + viewdata.getTarget()); notifications.setPairedItem(position, newViewData); updateAdapter(); @@ -620,7 +710,7 @@ public class NotificationsFragment extends SFragment @Override public void onViewMedia(int position, int attachmentIndex, @Nullable View view) { Notification notification = notifications.get(position).asRightOrNull(); - if(notification == null || notification.getStatus() == null) { + if (notification == null || notification.getStatus() == null) { return; } super.viewMedia(attachmentIndex, notification.getStatus(), view); @@ -635,7 +725,7 @@ public class NotificationsFragment extends SFragment @Override public void onViewReplyTo(int position) { Notification notification = notifications.get(position).asRightOrNull(); - if(notification == null) { + if (notification == null) { return; } super.onShowReplyTo(notification.getStatus().getInReplyToId()); @@ -650,13 +740,20 @@ public class NotificationsFragment extends SFragment @Override public void onExpandedChange(boolean expanded, int position) { NotificationViewData.Concrete old = - (NotificationViewData.Concrete) notifications.getPairedItem(position); + (NotificationViewData.Concrete) notifications.getPairedItem(position); StatusViewData.Concrete statusViewData = - new StatusViewData.Builder(old.getStatusViewData()).setIsExpanded(expanded) - .createStatusViewData(); + new StatusViewData.Builder(old.getStatusViewData()) + .setIsExpanded(expanded) + .createStatusViewData(); NotificationViewData notificationViewData = - new NotificationViewData.Concrete(old.getType(), old.getId(), old.getAccount(), - statusViewData, old.getEmoji(), old.getEmojiUrl(), old.getTarget()); + new NotificationViewData.Concrete( + old.getType(), + old.getId(), + old.getAccount(), + statusViewData, + old.getEmoji(), + old.getEmojiUrl(), + old.getTarget()); notifications.setPairedItem(position, notificationViewData); updateAdapter(); } @@ -664,13 +761,20 @@ public class NotificationsFragment extends SFragment @Override public void onContentHiddenChange(boolean isShowing, int position) { NotificationViewData.Concrete old = - (NotificationViewData.Concrete) notifications.getPairedItem(position); + (NotificationViewData.Concrete) notifications.getPairedItem(position); StatusViewData.Concrete statusViewData = - new StatusViewData.Builder(old.getStatusViewData()).setIsShowingSensitiveContent( - isShowing).createStatusViewData(); + new StatusViewData.Builder(old.getStatusViewData()) + .setIsShowingSensitiveContent(isShowing) + .createStatusViewData(); NotificationViewData notificationViewData = - new NotificationViewData.Concrete(old.getType(), old.getId(), old.getAccount(), - statusViewData, old.getEmoji(), old.getEmojiUrl(), old.getTarget()); + new NotificationViewData.Concrete( + old.getType(), + old.getId(), + old.getAccount(), + statusViewData, + old.getEmoji(), + old.getEmojiUrl(), + old.getTarget()); notifications.setPairedItem(position, notificationViewData); updateAdapter(); } @@ -678,54 +782,64 @@ public class NotificationsFragment extends SFragment @Override public void onMute(int position, boolean isMuted) { NotificationViewData.Concrete old = - (NotificationViewData.Concrete) notifications.getPairedItem(position); + (NotificationViewData.Concrete) notifications.getPairedItem(position); StatusViewData.Concrete statusViewData = - new StatusViewData.Builder(old.getStatusViewData()).setMuted(isMuted) - .createStatusViewData(); + new StatusViewData.Builder(old.getStatusViewData()) + .setMuted(isMuted) + .createStatusViewData(); NotificationViewData notificationViewData = - new NotificationViewData.Concrete(old.getType(), old.getId(), old.getAccount(), - statusViewData, old.getEmoji(), old.getEmojiUrl(), old.getTarget()); + new NotificationViewData.Concrete( + old.getType(), + old.getId(), + old.getAccount(), + statusViewData, + old.getEmoji(), + old.getEmojiUrl(), + old.getTarget()); notifications.setPairedItem(position, notificationViewData); updateAdapter(); } - private void setMutedStatusForStatus(int position, Status status, boolean muted, - boolean threadMuted) - { + private void setMutedStatusForStatus( + int position, Status status, boolean muted, boolean threadMuted) { status.setThreadMuted(threadMuted); NotificationViewData.Concrete viewdata = - (NotificationViewData.Concrete) notifications.getPairedItem(position); + (NotificationViewData.Concrete) notifications.getPairedItem(position); StatusViewData.Builder viewDataBuilder = - new StatusViewData.Builder(viewdata.getStatusViewData()); + new StatusViewData.Builder(viewdata.getStatusViewData()); viewDataBuilder.setThreadMuted(threadMuted); viewDataBuilder.setMuted(muted); NotificationViewData.Concrete newViewData = - new NotificationViewData.Concrete(viewdata.getType(), viewdata.getId(), - viewdata.getAccount(), viewDataBuilder.createStatusViewData(), viewdata.getEmoji(), - viewdata.getEmojiUrl(), viewdata.getTarget()); + new NotificationViewData.Concrete( + viewdata.getType(), + viewdata.getId(), + viewdata.getAccount(), + viewDataBuilder.createStatusViewData(), + viewdata.getEmoji(), + viewdata.getEmojiUrl(), + viewdata.getTarget()); notifications.setPairedItem(position, newViewData); } - @Override public void onLoadMore(int position) { - //check bounds before accessing list, - if(notifications.size() >= position && position > 0) { + // check bounds before accessing list, + if (notifications.size() >= position && position > 0) { Notification previous = notifications.get(position - 1).asRightOrNull(); Notification next = notifications.get(position + 1).asRightOrNull(); - if(previous == null || next == null) { + if (previous == null || next == null) { Log.e(TAG, "Failed to load more, invalid placeholder position: " + position); return; } - sendFetchNotificationsRequest(previous.getId(), next.getId(), FetchEnd.MIDDLE, - position); + sendFetchNotificationsRequest( + previous.getId(), next.getId(), FetchEnd.MIDDLE, position); Placeholder placeholder = notifications.get(position).asLeft(); NotificationViewData notificationViewData = - new NotificationViewData.Placeholder(placeholder.id, true); + new NotificationViewData.Placeholder(placeholder.id, true); notifications.setPairedItem(position, notificationViewData); updateAdapter(); } else { @@ -735,34 +849,44 @@ public class NotificationsFragment extends SFragment @Override public void onContentCollapsedChange(boolean isCollapsed, int position) { - if(position < 0 || position >= notifications.size()) { - Log.e(TAG, - String.format("Tried to access out of bounds status position: %d of %d", position, - notifications.size() - 1)); + if (position < 0 || position >= notifications.size()) { + Log.e( + TAG, + String.format( + "Tried to access out of bounds status position: %d of %d", + position, notifications.size() - 1)); return; } NotificationViewData notification = notifications.getPairedItem(position); - if(!(notification instanceof NotificationViewData.Concrete)) { - Log.e(TAG, String.format( - "Expected NotificationViewData.Concrete, got %s instead at position: %d of %d", - notification == null ? "null" : notification.getClass().getSimpleName(), position, - notifications.size() - 1)); + if (!(notification instanceof NotificationViewData.Concrete)) { + Log.e( + TAG, + String.format( + "Expected NotificationViewData.Concrete, got %s instead at position: %d" + + " of %d", + notification == null ? "null" : notification.getClass().getSimpleName(), + position, + notifications.size() - 1)); return; } StatusViewData.Concrete status = - ((NotificationViewData.Concrete) notification).getStatusViewData(); + ((NotificationViewData.Concrete) notification).getStatusViewData(); StatusViewData.Concrete updatedStatus = - new StatusViewData.Builder(status).setCollapsed(isCollapsed).createStatusViewData(); + new StatusViewData.Builder(status).setCollapsed(isCollapsed).createStatusViewData(); NotificationViewData.Concrete concreteNotification = - (NotificationViewData.Concrete) notification; + (NotificationViewData.Concrete) notification; NotificationViewData updatedNotification = - new NotificationViewData.Concrete(concreteNotification.getType(), - concreteNotification.getId(), concreteNotification.getAccount(), updatedStatus, - concreteNotification.getEmoji(), concreteNotification.getEmojiUrl(), - concreteNotification.getTarget()); + new NotificationViewData.Concrete( + concreteNotification.getType(), + concreteNotification.getId(), + concreteNotification.getAccount(), + updatedStatus, + concreteNotification.getEmoji(), + concreteNotification.getEmojiUrl(), + concreteNotification.getTarget()); notifications.setPairedItem(position, updatedNotification); updateAdapter(); @@ -779,64 +903,64 @@ public class NotificationsFragment extends SFragment } private void clearNotifications() { - //Cancel all ongoing requests + // Cancel all ongoing requests swipeRefreshLayout.setRefreshing(false); resetNotificationsLoad(); - //Show friend elephant + // Show friend elephant this.statusView.setVisibility(View.VISIBLE); this.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null); updateFilterVisibility(); - //Update adapter + // Update adapter updateAdapter(); - //Execute clear notifications request + // Execute clear notifications request Call<ResponseBody> call = mastodonApi.getValue().clearNotifications(); - call.enqueue(new Callback<ResponseBody>() { - @Override - public void onResponse(@NonNull Call<ResponseBody> call, - @NonNull Response<ResponseBody> response) - { - if(isAdded()) { - if(!response.isSuccessful()) { - //Reload notifications on failure - fullyRefreshWithProgressBar(true); + call.enqueue( + new Callback<ResponseBody>() { + @Override + public void onResponse( + @NonNull Call<ResponseBody> call, + @NonNull Response<ResponseBody> response) { + if (isAdded()) { + if (!response.isSuccessful()) { + // Reload notifications on failure + fullyRefreshWithProgressBar(true); + } + } } - } - } - @Override - public void onFailure(@NonNull Call<ResponseBody> call, @NonNull Throwable t) { - //Reload notifications on failure - fullyRefreshWithProgressBar(true); - } - }); + @Override + public void onFailure(@NonNull Call<ResponseBody> call, @NonNull Throwable t) { + // Reload notifications on failure + fullyRefreshWithProgressBar(true); + } + }); callList.add(call); } private void resetNotificationsLoad() { - for(Call callItem : callList) { + for (Call callItem : callList) { callItem.cancel(); } callList.clear(); bottomLoading = false; topLoading = false; - //Disable load more + // Disable load more bottomId = null; - //Clear exists notifications + // Clear exists notifications notifications.clear(); } - private void showFilterMenu() { List<Notification.Type> notificationsList = Notification.Type.Companion.getAsList(); List<String> list = new ArrayList<>(); - for(Notification.Type type : notificationsList) { + for (Notification.Type type : notificationsList) { // ignore chat messages, as we don't work with them in main notification fragment - if(type == Notification.Type.CHAT_MESSAGE) { + if (type == Notification.Type.CHAT_MESSAGE) { continue; } @@ -844,29 +968,31 @@ public class NotificationsFragment extends SFragment } ArrayAdapter<String> adapter = - new ArrayAdapter<>(getContext(), android.R.layout.simple_list_item_multiple_choice, - list); + new ArrayAdapter<>( + getContext(), android.R.layout.simple_list_item_multiple_choice, list); PopupWindow window = new PopupWindow(getContext()); - View view = LayoutInflater.from(getContext()) - .inflate(R.layout.notifications_filter, (ViewGroup) getView(), false); + View view = + LayoutInflater.from(getContext()) + .inflate(R.layout.notifications_filter, (ViewGroup) getView(), false); final ListView listView = view.findViewById(R.id.listView); - view.findViewById(R.id.buttonApply).setOnClickListener(v -> { - SparseBooleanArray checkedItems = listView.getCheckedItemPositions(); - Set<Notification.Type> excludes = new HashSet<>(); - for(int i = 0; i < notificationsList.size(); i++) { - if(!checkedItems.get(i, false)) { - excludes.add(notificationsList.get(i)); - } - } - window.dismiss(); - applyFilterChanges(excludes); - - }); + view.findViewById(R.id.buttonApply) + .setOnClickListener( + v -> { + SparseBooleanArray checkedItems = listView.getCheckedItemPositions(); + Set<Notification.Type> excludes = new HashSet<>(); + for (int i = 0; i < notificationsList.size(); i++) { + if (!checkedItems.get(i, false)) { + excludes.add(notificationsList.get(i)); + } + } + window.dismiss(); + applyFilterChanges(excludes); + }); listView.setAdapter(adapter); listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); - for(int i = 0; i < notificationsList.size(); i++) { - if(!notificationFilter.contains(notificationsList.get(i))) { + for (int i = 0; i < notificationsList.size(); i++) { + if (!notificationFilter.contains(notificationsList.get(i))) { listView.setItemChecked(i, true); } } @@ -875,11 +1001,10 @@ public class NotificationsFragment extends SFragment window.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT); window.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); window.showAsDropDown(buttonFilter); - } private String getNotificationText(Notification.Type type) { - switch(type) { + switch (type) { case MENTION: return getString(R.string.notification_mention_name); case FAVOURITE: @@ -906,36 +1031,35 @@ public class NotificationsFragment extends SFragment private void applyFilterChanges(Set<Notification.Type> newSet) { List<Notification.Type> notifications = Notification.Type.Companion.getAsList(); boolean isChanged = false; - for(Notification.Type type : notifications) { - if(notificationFilter.contains(type) && !newSet.contains(type)) { + for (Notification.Type type : notifications) { + if (notificationFilter.contains(type) && !newSet.contains(type)) { notificationFilter.remove(type); isChanged = true; - } else if(!notificationFilter.contains(type) && newSet.contains(type)) { + } else if (!notificationFilter.contains(type) && newSet.contains(type)) { notificationFilter.add(type); isChanged = true; } } - if(isChanged) { + if (isChanged) { saveNotificationsFilter(); fullyRefreshWithProgressBar(true); } - } private void loadNotificationsFilter() { AccountEntity account = accountManager.getValue().getActiveAccount(); - if(account != null) { + if (account != null) { notificationFilter.clear(); notificationFilter.addAll( - NotificationTypeConverterKt.deserialize(account.getNotificationsFilter())); + NotificationTypeConverterKt.deserialize(account.getNotificationsFilter())); } } private void saveNotificationsFilter() { AccountEntity account = accountManager.getValue().getActiveAccount(); - if(account != null) { + if (account != null) { account.setNotificationsFilter( - NotificationTypeConverterKt.serialize(notificationFilter)); + NotificationTypeConverterKt.serialize(notificationFilter)); accountManager.getValue().saveAccount(account); } } @@ -963,19 +1087,26 @@ public class NotificationsFragment extends SFragment @Override public void onRespondToFollowRequest(boolean accept, String id, int position) { Single<Relationship> request = - accept ? mastodonApi.getValue().authorizeFollowRequestObservable(id) : - mastodonApi.getValue().rejectFollowRequestObservable(id); + accept + ? mastodonApi.getValue().authorizeFollowRequestObservable(id) + : mastodonApi.getValue().rejectFollowRequestObservable(id); request.observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) - .subscribe((relationship) -> fullyRefreshWithProgressBar(true), (error) -> Log.e(TAG, - String.format("Failed to %s account id %s", accept ? "accept" : "reject", id))); + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + (relationship) -> fullyRefreshWithProgressBar(true), + (error) -> + Log.e( + TAG, + String.format( + "Failed to %s account id %s", + accept ? "accept" : "reject", id))); } @Override public void onViewStatusForNotificationId(String notificationId) { - for(Either<Placeholder, Notification> either : notifications) { + for (Either<Placeholder, Notification> either : notifications) { Notification notification = either.asRightOrNull(); - if(notification != null && notification.getId().equals(notificationId)) { + if (notification != null && notification.getId().equals(notificationId)) { super.viewThread(notification.getStatus()); return; } @@ -985,33 +1116,37 @@ public class NotificationsFragment extends SFragment private void onPreferenceChanged(String key) { SharedPreferences sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(getContext()); + PreferenceManager.getDefaultSharedPreferences(getContext()); - switch(key) { - case "fabHide": { - hideFab = sharedPreferences.getBoolean("fabHide", false); - break; - } - case "mediaPreviewEnabled": { - boolean enabled = - accountManager.getValue().getActiveAccount().getMediaPreviewEnabled(); - if(enabled != adapter.isMediaPreviewEnabled()) { - adapter.setMediaPreviewEnabled(enabled); - fullyRefresh(); + switch (key) { + case "fabHide": + { + hideFab = sharedPreferences.getBoolean("fabHide", false); + break; } - } - case "showNotificationsFilter": { - if(isAdded()) { - showNotificationsFilter = - sharedPreferences.getBoolean("showNotificationsFilter", true); - updateFilterVisibility(); - fullyRefreshWithProgressBar(true); + case "mediaPreviewEnabled": + { + boolean enabled = + accountManager.getValue().getActiveAccount().getMediaPreviewEnabled(); + if (enabled != adapter.isMediaPreviewEnabled()) { + adapter.setMediaPreviewEnabled(enabled); + fullyRefresh(); + } + } + case "showNotificationsFilter": + { + if (isAdded()) { + showNotificationsFilter = + sharedPreferences.getBoolean("showNotificationsFilter", true); + updateFilterVisibility(); + fullyRefreshWithProgressBar(true); + } + } + case PrefKeys.HIDE_MUTED_USERS: + { + withMuted = !sharedPreferences.getBoolean(PrefKeys.HIDE_MUTED_USERS, false); + fullyRefresh(); } - } - case PrefKeys.HIDE_MUTED_USERS: { - withMuted = !sharedPreferences.getBoolean(PrefKeys.HIDE_MUTED_USERS, false); - fullyRefresh(); - } } } @@ -1024,12 +1159,16 @@ public class NotificationsFragment extends SFragment private void removeAllByConversationId(String conversationId) { // using iterator to safely remove items while iterating Iterator<Either<Placeholder, Notification>> iterator = notifications.iterator(); - while(iterator.hasNext()) { + while (iterator.hasNext()) { Either<Placeholder, Notification> placeholderOrNotification = iterator.next(); Notification notification = placeholderOrNotification.asRightOrNull(); - if(notification != null && notification.getStatus() != null && - notification.getType() == Notification.Type.MENTION && - notification.getStatus().getConversationId().equalsIgnoreCase(conversationId)) { + if (notification != null + && notification.getStatus() != null + && notification.getType() == Notification.Type.MENTION + && notification + .getStatus() + .getConversationId() + .equalsIgnoreCase(conversationId)) { iterator.remove(); } } @@ -1039,11 +1178,11 @@ public class NotificationsFragment extends SFragment private void removeAllByAccountId(String accountId) { // using iterator to safely remove items while iterating Iterator<Either<Placeholder, Notification>> iterator = notifications.iterator(); - while(iterator.hasNext()) { + while (iterator.hasNext()) { Either<Placeholder, Notification> notification = iterator.next(); Notification maybeNotification = notification.asRightOrNull(); - if(maybeNotification != null && - maybeNotification.getAccount().getId().equals(accountId)) { + if (maybeNotification != null + && maybeNotification.getAccount().getId().equals(accountId)) { iterator.remove(); } } @@ -1051,7 +1190,7 @@ public class NotificationsFragment extends SFragment } private void onLoadMore() { - if(bottomId == null) { + if (bottomId == null) { // already loaded everything return; } @@ -1059,13 +1198,13 @@ public class NotificationsFragment extends SFragment // Check for out-of-bounds when loading // This is required to allow full-timeline reloads of collapsible statuses when the settings // change. - if(notifications.size() > 0) { + if (notifications.size() > 0) { Either<Placeholder, Notification> last = notifications.get(notifications.size() - 1); - if(last.isRight()) { + if (last.isRight()) { final Placeholder placeholder = newPlaceholder(); notifications.add(new Either.Left<>(placeholder)); NotificationViewData viewData = - new NotificationViewData.Placeholder(placeholder.id, true); + new NotificationViewData.Placeholder(placeholder.id, true); notifications.setPairedItem(notifications.size() - 1, viewData); updateAdapter(); } @@ -1081,105 +1220,114 @@ public class NotificationsFragment extends SFragment } private void jumpToTop() { - if(isAdded()) { + if (isAdded()) { appBarOptions.setExpanded(true, false); layoutManager.scrollToPosition(0); scrollListener.reset(); } } - private void sendFetchNotificationsRequest(String fromId, String uptoId, - final FetchEnd fetchEnd, final int pos) - { + private void sendFetchNotificationsRequest( + String fromId, String uptoId, final FetchEnd fetchEnd, final int pos) { /* If there is a fetch already ongoing, record however many fetches are requested and * fulfill them after it's complete. */ - if(fetchEnd == FetchEnd.TOP && topLoading) { + if (fetchEnd == FetchEnd.TOP && topLoading) { return; } - if(fetchEnd == FetchEnd.BOTTOM && bottomLoading) { + if (fetchEnd == FetchEnd.BOTTOM && bottomLoading) { return; } - if(fetchEnd == FetchEnd.TOP) { + if (fetchEnd == FetchEnd.TOP) { topLoading = true; } - if(fetchEnd == FetchEnd.BOTTOM) { + if (fetchEnd == FetchEnd.BOTTOM) { bottomLoading = true; } - Call<List<Notification>> call = mastodonApi.getValue() - .notifications(fromId, uptoId, LOAD_AT_ONCE, - showNotificationsFilter ? notificationFilter : null, withMuted); - - call.enqueue(new Callback<List<Notification>>() { - @Override - public void onResponse(@NonNull Call<List<Notification>> call, - @NonNull Response<List<Notification>> response) - { - if(response.isSuccessful()) { - String linkHeader = response.headers().get("Link"); - onFetchNotificationsSuccess(response.body(), linkHeader, fetchEnd, pos); - } else { - onFetchNotificationsFailure(new Exception(response.message()), fetchEnd, pos); - } - } + Call<List<Notification>> call = + mastodonApi + .getValue() + .notifications( + fromId, + uptoId, + LOAD_AT_ONCE, + showNotificationsFilter ? notificationFilter : null, + withMuted); + + call.enqueue( + new Callback<List<Notification>>() { + @Override + public void onResponse( + @NonNull Call<List<Notification>> call, + @NonNull Response<List<Notification>> response) { + if (response.isSuccessful()) { + String linkHeader = response.headers().get("Link"); + onFetchNotificationsSuccess(response.body(), linkHeader, fetchEnd, pos); + } else { + onFetchNotificationsFailure( + new Exception(response.message()), fetchEnd, pos); + } + } - @Override - public void onFailure(@NonNull Call<List<Notification>> call, @NonNull Throwable t) { - if(!call.isCanceled()) { - onFetchNotificationsFailure((Exception) t, fetchEnd, pos); - } - } - }); + @Override + public void onFailure( + @NonNull Call<List<Notification>> call, @NonNull Throwable t) { + if (!call.isCanceled()) { + onFetchNotificationsFailure((Exception) t, fetchEnd, pos); + } + } + }); callList.add(call); } - private void onFetchNotificationsSuccess(List<Notification> notifications, String linkHeader, - FetchEnd fetchEnd, int pos) - { + private void onFetchNotificationsSuccess( + List<Notification> notifications, String linkHeader, FetchEnd fetchEnd, int pos) { List<HttpHeaderLink> links = HttpHeaderLink.parse(linkHeader); HttpHeaderLink next = HttpHeaderLink.findByRelationType(links, "next"); String fromId = null; - if(next != null) { + if (next != null) { fromId = next.uri.getQueryParameter("max_id"); } - switch(fetchEnd) { - case TOP: { - update(notifications, this.notifications.isEmpty() ? fromId : null); - break; - } - case MIDDLE: { - replacePlaceholderWithNotifications(notifications, pos); - break; - } - case BOTTOM: { - - if(!this.notifications.isEmpty() && - !this.notifications.get(this.notifications.size() - 1).isRight()) { - this.notifications.remove(this.notifications.size() - 1); - updateAdapter(); + switch (fetchEnd) { + case TOP: + { + update(notifications, this.notifications.isEmpty() ? fromId : null); + break; } - - if(adapter.getItemCount() > 1) { - addItems(notifications, fromId); - } else { - update(notifications, fromId); + case MIDDLE: + { + replacePlaceholderWithNotifications(notifications, pos); + break; } + case BOTTOM: + { + if (!this.notifications.isEmpty() + && !this.notifications.get(this.notifications.size() - 1).isRight()) { + this.notifications.remove(this.notifications.size() - 1); + updateAdapter(); + } - break; - } + if (adapter.getItemCount() > 1) { + addItems(notifications, fromId); + } else { + update(notifications, fromId); + } + + break; + } } saveNewestNotificationId(notifications); - if(fetchEnd == FetchEnd.TOP) { + if (fetchEnd == FetchEnd.TOP) { topLoading = false; } - if(fetchEnd == FetchEnd.BOTTOM) { + if (fetchEnd == FetchEnd.BOTTOM) { bottomLoading = false; } - if(notifications.size() == 0 && adapter.getItemCount() == 0) { + if (notifications.size() == 0 && adapter.getItemCount() == 0) { this.statusView.setVisibility(View.VISIBLE); this.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null); } else { @@ -1192,37 +1340,43 @@ public class NotificationsFragment extends SFragment private void onFetchNotificationsFailure(Exception exception, FetchEnd fetchEnd, int position) { swipeRefreshLayout.setRefreshing(false); - if(fetchEnd == FetchEnd.MIDDLE && !notifications.get(position).isRight()) { + if (fetchEnd == FetchEnd.MIDDLE && !notifications.get(position).isRight()) { Placeholder placeholder = notifications.get(position).asLeft(); NotificationViewData placeholderVD = - new NotificationViewData.Placeholder(placeholder.id, false); + new NotificationViewData.Placeholder(placeholder.id, false); notifications.setPairedItem(position, placeholderVD); updateAdapter(); - } else if(this.notifications.isEmpty()) { + } else if (this.notifications.isEmpty()) { this.statusView.setVisibility(View.VISIBLE); swipeRefreshLayout.setEnabled(false); this.showingError = true; - if(exception instanceof IOException) { - this.statusView.setup(R.drawable.elephant_offline, R.string.error_network, __ -> { - this.progressBar.setVisibility(View.VISIBLE); - this.onRefresh(); - return Unit.INSTANCE; - }); + if (exception instanceof IOException) { + this.statusView.setup( + R.drawable.elephant_offline, + R.string.error_network, + __ -> { + this.progressBar.setVisibility(View.VISIBLE); + this.onRefresh(); + return Unit.INSTANCE; + }); } else { - this.statusView.setup(R.drawable.elephant_error, R.string.error_generic, __ -> { - this.progressBar.setVisibility(View.VISIBLE); - this.onRefresh(); - return Unit.INSTANCE; - }); + this.statusView.setup( + R.drawable.elephant_error, + R.string.error_generic, + __ -> { + this.progressBar.setVisibility(View.VISIBLE); + this.onRefresh(); + return Unit.INSTANCE; + }); } updateFilterVisibility(); } Log.e(TAG, "Fetch failure: " + exception.getMessage()); - if(fetchEnd == FetchEnd.TOP) { + if (fetchEnd == FetchEnd.TOP) { topLoading = false; } - if(fetchEnd == FetchEnd.BOTTOM) { + if (fetchEnd == FetchEnd.BOTTOM) { bottomLoading = false; } @@ -1232,16 +1386,16 @@ public class NotificationsFragment extends SFragment private void saveNewestNotificationId(List<Notification> notifications) { AccountEntity account = accountManager.getValue().getActiveAccount(); - if(account != null) { + if (account != null) { String lastNotificationId = account.getLastNotificationId(); - for(Notification noti : notifications) { - if(isLessThan(lastNotificationId, noti.getId())) { + for (Notification noti : notifications) { + if (isLessThan(lastNotificationId, noti.getId())) { lastNotificationId = noti.getId(); } } - if(!account.getLastNotificationId().equals(lastNotificationId)) { + if (!account.getLastNotificationId().equals(lastNotificationId)) { Log.d(TAG, "saving newest noti id: " + lastNotificationId); account.setLastNotificationId(lastNotificationId); accountManager.getValue().saveAccount(account); @@ -1250,25 +1404,25 @@ public class NotificationsFragment extends SFragment } private void update(@Nullable List<Notification> newNotifications, @Nullable String fromId) { - if(ListUtils.isEmpty(newNotifications)) { + if (ListUtils.isEmpty(newNotifications)) { updateAdapter(); return; } - if(fromId != null) { + if (fromId != null) { bottomId = fromId; } List<Either<Placeholder, Notification>> liftedNew = liftNotificationList(newNotifications); - if(notifications.isEmpty()) { + if (notifications.isEmpty()) { notifications.addAll(liftedNew); } else { int index = notifications.indexOf(liftedNew.get(newNotifications.size() - 1)); - for(int i = 0; i < index; i++) { + for (int i = 0; i < index; i++) { notifications.remove(0); } int newIndex = liftedNew.indexOf(notifications.get(0)); - if(newIndex == -1) { - if(index == -1 && liftedNew.size() >= LOAD_AT_ONCE) { + if (newIndex == -1) { + if (index == -1 && liftedNew.size() >= LOAD_AT_ONCE) { liftedNew.add(new Either.Left<>(newPlaceholder())); } notifications.addAll(0, liftedNew); @@ -1281,13 +1435,13 @@ public class NotificationsFragment extends SFragment private void addItems(List<Notification> newNotifications, @Nullable String fromId) { bottomId = fromId; - if(ListUtils.isEmpty(newNotifications)) { + if (ListUtils.isEmpty(newNotifications)) { return; } int end = notifications.size(); List<Either<Placeholder, Notification>> liftedNew = liftNotificationList(newNotifications); Either<Placeholder, Notification> last = notifications.get(end - 1); - if(last != null && liftedNew.indexOf(last) == -1) { + if (last != null && liftedNew.indexOf(last) == -1) { notifications.addAll(liftedNew); updateAdapter(); } @@ -1297,7 +1451,7 @@ public class NotificationsFragment extends SFragment // Remove placeholder notifications.remove(pos); - if(ListUtils.isEmpty(newNotifications)) { + if (ListUtils.isEmpty(newNotifications)) { updateAdapter(); return; } @@ -1307,7 +1461,7 @@ public class NotificationsFragment extends SFragment // If we fetched less posts than in the limit, it means that the hole is not filled // If we fetched at least as much it means that there are more posts to load and we should // insert new placeholder - if(newNotifications.size() >= LOAD_AT_ONCE) { + if (newNotifications.size() >= LOAD_AT_ONCE) { liftedNew.add(new Either.Left<>(newPlaceholder())); } @@ -1316,7 +1470,7 @@ public class NotificationsFragment extends SFragment } private final Function1<Notification, Either<Placeholder, Notification>> notificationLifter = - Either.Right::new; + Either.Right::new; private List<Either<Placeholder, Notification>> liftNotificationList(List<Notification> list) { return CollectionsKt.map(list, notificationLifter); @@ -1324,7 +1478,7 @@ public class NotificationsFragment extends SFragment private void fullyRefreshWithProgressBar(boolean isShow) { resetNotificationsLoad(); - if(isShow) { + if (isShow) { progressBar.setVisibility(View.VISIBLE); statusView.setVisibility(View.GONE); } @@ -1338,13 +1492,15 @@ public class NotificationsFragment extends SFragment @Nullable private Pair<Integer, Notification> findReplyPosition(@NonNull String statusId) { - for(int i = 0; i < notifications.size(); i++) { + for (int i = 0; i < notifications.size(); i++) { Notification notification = notifications.get(i).asRightOrNull(); - if(notification != null && notification.getStatus() != null && - notification.getType() == Notification.Type.MENTION && - (statusId.equals(notification.getStatus().getId()) || - (notification.getStatus().getReblog() != null && - statusId.equals(notification.getStatus().getReblog().getId())))) { + if (notification != null + && notification.getStatus() != null + && notification.getType() == Notification.Type.MENTION + && (statusId.equals(notification.getStatus().getId()) + || (notification.getStatus().getReblog() != null + && statusId.equals( + notification.getStatus().getReblog().getId())))) { return new Pair<>(i, notification); } } @@ -1355,94 +1511,94 @@ public class NotificationsFragment extends SFragment differ.submitList(notifications.getPairedCopy()); } - private final ListUpdateCallback listUpdateCallback = new ListUpdateCallback() { - @Override - public void onInserted(int position, int count) { - if(isAdded()) { - adapter.notifyItemRangeInserted(position, count); - Context context = getContext(); - // scroll up when new items at the top are loaded while being at the start - // https://github.com/tuskyapp/Tusky/pull/1905#issuecomment-677819724 - if(position == 0 && context != null && adapter.getItemCount() != count) { - recyclerView.scrollBy(0, Utils.dpToPx(context, -30)); + private final ListUpdateCallback listUpdateCallback = + new ListUpdateCallback() { + @Override + public void onInserted(int position, int count) { + if (isAdded()) { + adapter.notifyItemRangeInserted(position, count); + Context context = getContext(); + // scroll up when new items at the top are loaded while being at the start + // https://github.com/tuskyapp/Tusky/pull/1905#issuecomment-677819724 + if (position == 0 && context != null && adapter.getItemCount() != count) { + recyclerView.scrollBy(0, Utils.dpToPx(context, -30)); + } + } } - } - } - @Override - public void onRemoved(int position, int count) { - adapter.notifyItemRangeRemoved(position, count); - } + @Override + public void onRemoved(int position, int count) { + adapter.notifyItemRangeRemoved(position, count); + } - @Override - public void onMoved(int fromPosition, int toPosition) { - adapter.notifyItemMoved(fromPosition, toPosition); - } + @Override + public void onMoved(int fromPosition, int toPosition) { + adapter.notifyItemMoved(fromPosition, toPosition); + } - @Override - public void onChanged(int position, int count, Object payload) { - adapter.notifyItemRangeChanged(position, count, payload); - } - }; + @Override + public void onChanged(int position, int count, Object payload) { + adapter.notifyItemRangeChanged(position, count, payload); + } + }; private final AsyncListDiffer<NotificationViewData> differ = - new AsyncListDiffer<>(listUpdateCallback, - new AsyncDifferConfig.Builder<>(diffCallback).build()); + new AsyncListDiffer<>( + listUpdateCallback, new AsyncDifferConfig.Builder<>(diffCallback).build()); private final NotificationsAdapter.AdapterDataSource<NotificationViewData> dataSource = - new NotificationsAdapter.AdapterDataSource<NotificationViewData>() { - @Override - public int getItemCount() { - return differ.getCurrentList().size(); - } + new NotificationsAdapter.AdapterDataSource<NotificationViewData>() { + @Override + public int getItemCount() { + return differ.getCurrentList().size(); + } - @Override - public NotificationViewData getItemAt(int pos) { - return differ.getCurrentList().get(pos); - } - }; + @Override + public NotificationViewData getItemAt(int pos) { + return differ.getCurrentList().get(pos); + } + }; private static final DiffUtil.ItemCallback<NotificationViewData> diffCallback = - new DiffUtil.ItemCallback<NotificationViewData>() { + new DiffUtil.ItemCallback<NotificationViewData>() { - @Override - public boolean areItemsTheSame(NotificationViewData oldItem, - NotificationViewData newItem) - { - return oldItem.getViewDataId() == newItem.getViewDataId(); - } + @Override + public boolean areItemsTheSame( + NotificationViewData oldItem, NotificationViewData newItem) { + return oldItem.getViewDataId() == newItem.getViewDataId(); + } - @Override - public boolean areContentsTheSame(@NonNull NotificationViewData oldItem, - @NonNull NotificationViewData newItem) - { - return false; - } + @Override + public boolean areContentsTheSame( + @NonNull NotificationViewData oldItem, + @NonNull NotificationViewData newItem) { + return false; + } - @Nullable - @Override - public Object getChangePayload(@NonNull NotificationViewData oldItem, - @NonNull NotificationViewData newItem) - { - if(oldItem.deepEquals(newItem)) { - //If items are equal - update timestamp only - return Collections.singletonList(StatusBaseViewHolder.Key.KEY_CREATED); - } else - // If items are different - update a whole view holder - { - return null; + @Nullable + @Override + public Object getChangePayload( + @NonNull NotificationViewData oldItem, + @NonNull NotificationViewData newItem) { + if (oldItem.deepEquals(newItem)) { + // If items are equal - update timestamp only + return Collections.singletonList(StatusBaseViewHolder.Key.KEY_CREATED); + } else + // If items are different - update a whole view holder + { + return null; + } } - } - }; + }; @Override public void onResume() { super.onResume(); String rawAccountNotificationFilter = - accountManager.getValue().getActiveAccount().getNotificationsFilter(); + accountManager.getValue().getActiveAccount().getNotificationsFilter(); Set<Notification.Type> accountNotificationFilter = - NotificationTypeConverterKt.deserialize(rawAccountNotificationFilter); - if(!notificationFilter.equals(accountNotificationFilter)) { + NotificationTypeConverterKt.deserialize(rawAccountNotificationFilter); + if (!notificationFilter.equals(accountNotificationFilter)) { loadNotificationsFilter(); fullyRefreshWithProgressBar(true); } @@ -1450,20 +1606,19 @@ public class NotificationsFragment extends SFragment } /** - * Start to update adapter every minute to refresh timestamp - * If setting absoluteTimeView is false - * Auto dispose observable on pause + * Start to update adapter every minute to refresh timestamp If setting absoluteTimeView is + * false Auto dispose observable on pause */ private void startUpdateTimestamp() { SharedPreferences preferences = - PreferenceManager.getDefaultSharedPreferences(getActivity()); + PreferenceManager.getDefaultSharedPreferences(getActivity()); boolean useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false); - if(!useAbsoluteTime) { - Observable.interval(1, TimeUnit.MINUTES).observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_PAUSE))) - .subscribe(interval -> updateAdapter()); + if (!useAbsoluteTime) { + Observable.interval(1, TimeUnit.MINUTES) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_PAUSE))) + .subscribe(interval -> updateAdapter()); } - } @Override @@ -1473,12 +1628,17 @@ public class NotificationsFragment extends SFragment private void setEmojiReactForStatus(int position, Status newStatus) { NotificationViewData.Concrete viewdata = - (NotificationViewData.Concrete) notifications.getPairedItem(position); + (NotificationViewData.Concrete) notifications.getPairedItem(position); NotificationViewData.Concrete newViewData = - new NotificationViewData.Concrete(viewdata.getType(), viewdata.getId(), - viewdata.getAccount(), ViewDataUtils.statusToViewData(newStatus, false, false), - viewdata.getEmoji(), viewdata.getEmojiUrl(), viewdata.getTarget()); + new NotificationViewData.Concrete( + viewdata.getType(), + viewdata.getId(), + viewdata.getAccount(), + ViewDataUtils.statusToViewData(newStatus, false, false), + viewdata.getEmoji(), + viewdata.getEmojiUrl(), + viewdata.getTarget()); notifications.setPairedItem(position, newViewData); updateAdapter(); @@ -1486,33 +1646,38 @@ public class NotificationsFragment extends SFragment private void handleEmojiReactEvent(EmojiReactEvent event) { Pair<Integer, Notification> posAndNotification = - findReplyPosition(event.getNewStatus().getActionableId()); - if(posAndNotification == null) { + findReplyPosition(event.getNewStatus().getActionableId()); + if (posAndNotification == null) { return; } //noinspection ConstantConditions setEmojiReactForStatus(posAndNotification.first, event.getNewStatus()); } - @Override public void onEmojiReact(final boolean react, final String emoji, final String statusId) { Pair<Integer, Notification> posAndNotification = findReplyPosition(statusId); - if(posAndNotification == null) { + if (posAndNotification == null) { return; } - timelineCases.getValue().react(emoji, statusId, react) - .observeOn(AndroidSchedulers.mainThread()).as(autoDisposable(from(this))) - .subscribe((newStatus) -> setEmojiReactForStatus(posAndNotification.first, newStatus), - (t) -> Log.d(TAG, "Failed to react with " + emoji + " on status: " + statusId, t)); - + timelineCases + .getValue() + .react(emoji, statusId, react) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this))) + .subscribe( + (newStatus) -> setEmojiReactForStatus(posAndNotification.first, newStatus), + (t) -> + Log.d( + TAG, + "Failed to react with " + emoji + " on status: " + statusId, + t)); } @Override - public void onEmojiReactMenu(@NonNull View view, final EmojiReaction emoji, - final String statusId) - { + public void onEmojiReactMenu( + @NonNull View view, final EmojiReaction emoji, final String statusId) { super.emojiReactMenu(statusId, emoji, view, this); } } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java b/husky/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java index 8b608da..b10a2f7 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java @@ -15,6 +15,10 @@ package com.keylesspalace.tusky.fragment; +import static com.uber.autodispose.AutoDispose.autoDisposable; +import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from; +import static org.koin.java.KoinJavaComponent.inject; + import android.Manifest; import android.app.DownloadManager; import android.content.ClipData; @@ -64,8 +68,6 @@ import com.keylesspalace.tusky.settings.PrefKeys; import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.view.MuteAccountDialog; import com.keylesspalace.tusky.viewdata.AttachmentViewData; -import static com.uber.autodispose.AutoDispose.autoDisposable; -import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from; import io.reactivex.android.schedulers.AndroidSchedulers; import java.util.ArrayList; import java.util.LinkedHashSet; @@ -75,7 +77,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import kotlin.Lazy; import kotlin.Unit; -import static org.koin.java.KoinJavaComponent.inject; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; @@ -115,7 +116,7 @@ public abstract class SFragment extends BaseFragment { @Override public void onAttach(@NonNull Context context) { super.onAttach(context); - if(context instanceof BottomSheetActivity) { + if (context instanceof BottomSheetActivity) { bottomSheetActivity = (BottomSheetActivity) context; } else { throw new IllegalStateException("Fragment must be attached to a BottomSheetActivity!"); @@ -123,7 +124,7 @@ public abstract class SFragment extends BaseFragment { } protected void openReblog(@Nullable final Status status) { - if(status == null) { + if (status == null) { return; } bottomSheetActivity.viewAccount(status.getAccount().getId()); @@ -156,10 +157,10 @@ public abstract class SFragment extends BaseFragment { mentionedUsernames.add(actionableStatus.getAccount().getUsername()); String loggedInUsername = null; AccountEntity activeAccount = accountManager.getValue().getActiveAccount(); - if(activeAccount != null) { + if (activeAccount != null) { loggedInUsername = activeAccount.getUsername(); } - for(Status.Mention mention : mentions) { + for (Status.Mention mention : mentions) { mentionedUsernames.add(mention.getUsername()); } mentionedUsernames.remove(loggedInUsername); @@ -175,9 +176,11 @@ public abstract class SFragment extends BaseFragment { getActivity().startActivity(intent); } - protected void emojiReactMenu(@NonNull final String statusId, - @NonNull final EmojiReaction reaction, View view, final StatusActionListener listener) - { + protected void emojiReactMenu( + @NonNull final String statusId, + @NonNull final EmojiReaction reaction, + View view, + final StatusActionListener listener) { PopupMenu popup = new PopupMenu(getContext(), view); popup.inflate(R.menu.emoji_reaction_more); @@ -185,23 +188,29 @@ public abstract class SFragment extends BaseFragment { menu.findItem(R.id.emoji_react).setVisible(!reaction.getMe()); menu.findItem(R.id.emoji_unreact).setVisible(reaction.getMe()); - popup.setOnMenuItemClickListener(item -> { - switch(item.getItemId()) { - case R.id.emoji_react: - listener.onEmojiReact(true, reaction.getName(), statusId); - return true; - case R.id.emoji_unreact: - listener.onEmojiReact(false, reaction.getName(), statusId); - return true; - case R.id.emoji_reacted_by: - Intent intent = AccountListActivity.newIntent(getContext(), - AccountListActivity.Type.REACTED, statusId, reaction.getName()); - ((BaseActivity) getActivity()).startActivityWithSlideInAnimation(intent); - - return true; - } - return false; - }); + popup.setOnMenuItemClickListener( + item -> { + switch (item.getItemId()) { + case R.id.emoji_react: + listener.onEmojiReact(true, reaction.getName(), statusId); + return true; + case R.id.emoji_unreact: + listener.onEmojiReact(false, reaction.getName(), statusId); + return true; + case R.id.emoji_reacted_by: + Intent intent = + AccountListActivity.newIntent( + getContext(), + AccountListActivity.Type.REACTED, + statusId, + reaction.getName()); + ((BaseActivity) getActivity()) + .startActivityWithSlideInAnimation(intent); + + return true; + } + return false; + }); popup.show(); } @@ -215,52 +224,58 @@ public abstract class SFragment extends BaseFragment { String loggedInAccountId = null; AccountEntity activeAccount = accountManager.getValue().getActiveAccount(); - if(activeAccount != null) { + if (activeAccount != null) { loggedInAccountId = activeAccount.getAccountId(); } PopupMenu popup = new PopupMenu(getContext(), view); // Give a different menu depending on whether this is the user's own toot or not. - if(loggedInAccountId == null || !loggedInAccountId.equals(accountId)) { + if (loggedInAccountId == null || !loggedInAccountId.equals(accountId)) { popup.inflate(R.menu.status_more); Menu menu = popup.getMenu(); menu.findItem(R.id.status_download_media) - .setVisible(!status.getAttachments().isEmpty()); + .setVisible(!status.getAttachments().isEmpty()); } else { popup.inflate(R.menu.status_more_for_user); Menu menu = popup.getMenu(); - switch(status.getVisibility()) { + switch (status.getVisibility()) { case PUBLIC: - case UNLISTED: { - final String textId = - getString(status.isPinned() ? R.string.unpin_action : R.string.pin_action); - menu.add(0, R.id.pin, 1, textId); - break; - } - case PRIVATE: { - boolean reblogged = status.getReblogged(); - if(status.getReblog() != null) { - reblogged = status.getReblog().getReblogged(); + case UNLISTED: + { + final String textId = + getString( + status.isPinned() + ? R.string.unpin_action + : R.string.pin_action); + menu.add(0, R.id.pin, 1, textId); + break; + } + case PRIVATE: + { + boolean reblogged = status.getReblogged(); + if (status.getReblog() != null) { + reblogged = status.getReblog().getReblogged(); + } + menu.findItem(R.id.status_reblog_private).setVisible(!reblogged); + menu.findItem(R.id.status_unreblog_private).setVisible(reblogged); + break; } - menu.findItem(R.id.status_reblog_private).setVisible(!reblogged); - menu.findItem(R.id.status_unreblog_private).setVisible(reblogged); - break; - } } } Menu menu = popup.getMenu(); MenuItem openAsItem = menu.findItem(R.id.status_open_as); - switch(accounts.size()) { + switch (accounts.size()) { case 0: case 1: openAsItem.setVisible(false); break; case 2: - for(AccountEntity account : accounts) { - if(account != activeAccount) { - openAsTitle = String.format(getString(R.string.action_open_as), - account.getFullName()); + for (AccountEntity account : accounts) { + if (account != activeAccount) { + openAsTitle = + String.format( + getString(R.string.action_open_as), account.getFullName()); break; } } @@ -272,10 +287,10 @@ public abstract class SFragment extends BaseFragment { openAsItem.setTitle(openAsTitle); // maybe not a best check - if(status.getPleroma() != null) { + if (status.getPleroma() != null) { boolean showMute = true; // predict state - if(status.isThreadMuted() == true) { + if (status.isThreadMuted() == true) { showMute = false; } @@ -285,121 +300,151 @@ public abstract class SFragment extends BaseFragment { menu.findItem(R.id.status_unmute_conversation).setVisible(!showMute); } - popup.setOnMenuItemClickListener(item -> { - switch(item.getItemId()) { - case R.id.status_share_content: { - Status statusToShare = status; - if(statusToShare.getReblog() != null) { - statusToShare = statusToShare.getReblog(); + popup.setOnMenuItemClickListener( + item -> { + switch (item.getItemId()) { + case R.id.status_share_content: + { + Status statusToShare = status; + if (statusToShare.getReblog() != null) { + statusToShare = statusToShare.getReblog(); + } + + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + + String stringToShare = + statusToShare.getAccount().getUsername() + + " - " + + statusToShare.getContent().toString(); + sendIntent.putExtra(Intent.EXTRA_TEXT, stringToShare); + sendIntent.putExtra(Intent.EXTRA_SUBJECT, statusUrl); + sendIntent.setType("text/plain"); + startActivity( + Intent.createChooser( + sendIntent, + getResources() + .getText(R.string.send_status_content_to))); + return true; + } + case R.id.status_share_link: + { + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, statusUrl); + sendIntent.setType("text/plain"); + startActivity( + Intent.createChooser( + sendIntent, + getResources() + .getText(R.string.send_status_link_to))); + return true; + } + case R.id.status_copy_link: + { + ClipboardManager clipboard = + (ClipboardManager) + getActivity() + .getSystemService( + Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText(null, statusUrl); + clipboard.setPrimaryClip(clip); + return true; + } + case R.id.status_open_in_web: + { + LinkHelper.openLinkInBrowser(Uri.parse(statusUrl), getContext()); + return true; + } + case R.id.status_open_as: + { + showOpenAsDialog(statusUrl, item.getTitle()); + return true; + } + case R.id.status_download_media: + { + requestDownloadAllMedia(status); + return true; + } + case R.id.status_mute: + { + onMute(accountId, accountUsername); + return true; + } + case R.id.status_block: + { + onBlock(accountId, accountUsername); + return true; + } + case R.id.status_report: + { + openReportPage(accountId, accountUsername, id); + return true; + } + case R.id.status_mute_conversation: + { + timelineCases.getValue().muteConversation(status, true); + return true; + } + case R.id.status_unmute_conversation: + { + timelineCases.getValue().muteConversation(status, false); + return true; + } + case R.id.status_unreblog_private: + { + onReblog(false, position); + return true; + } + case R.id.status_reblog_private: + { + onReblog(true, position); + return true; + } + case R.id.status_delete: + { + showConfirmDeleteDialog(id, position); + return true; + } + case R.id.pin: + { + timelineCases.getValue().pin(status, !status.isPinned()); + return true; + } } - - Intent sendIntent = new Intent(); - sendIntent.setAction(Intent.ACTION_SEND); - - String stringToShare = statusToShare.getAccount().getUsername() + " - " + - statusToShare.getContent().toString(); - sendIntent.putExtra(Intent.EXTRA_TEXT, stringToShare); - sendIntent.putExtra(Intent.EXTRA_SUBJECT, statusUrl); - sendIntent.setType("text/plain"); - startActivity(Intent.createChooser(sendIntent, - getResources().getText(R.string.send_status_content_to))); - return true; - } - case R.id.status_share_link: { - Intent sendIntent = new Intent(); - sendIntent.setAction(Intent.ACTION_SEND); - sendIntent.putExtra(Intent.EXTRA_TEXT, statusUrl); - sendIntent.setType("text/plain"); - startActivity(Intent.createChooser(sendIntent, - getResources().getText(R.string.send_status_link_to))); - return true; - } - case R.id.status_copy_link: { - ClipboardManager clipboard = (ClipboardManager) getActivity().getSystemService( - Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText(null, statusUrl); - clipboard.setPrimaryClip(clip); - return true; - } - case R.id.status_open_in_web: { - LinkHelper.openLinkInBrowser(Uri.parse(statusUrl), getContext()); - return true; - } - case R.id.status_open_as: { - showOpenAsDialog(statusUrl, item.getTitle()); - return true; - } - case R.id.status_download_media: { - requestDownloadAllMedia(status); - return true; - } - case R.id.status_mute: { - onMute(accountId, accountUsername); - return true; - } - case R.id.status_block: { - onBlock(accountId, accountUsername); - return true; - } - case R.id.status_report: { - openReportPage(accountId, accountUsername, id); - return true; - } - case R.id.status_mute_conversation: { - timelineCases.getValue().muteConversation(status, true); - return true; - } - case R.id.status_unmute_conversation: { - timelineCases.getValue().muteConversation(status, false); - return true; - } - case R.id.status_unreblog_private: { - onReblog(false, position); - return true; - } - case R.id.status_reblog_private: { - onReblog(true, position); - return true; - } - case R.id.status_delete: { - showConfirmDeleteDialog(id, position); - return true; - } - case R.id.pin: { - timelineCases.getValue().pin(status, !status.isPinned()); - return true; - } - } - return false; - }); + return false; + }); popup.show(); } private void onMute(String accountId, String accountUsername) { - MuteAccountDialog.showMuteAccountDialog(this.getActivity(), accountUsername, - (notifications, duration) -> { - timelineCases.getValue().mute(accountId, notifications, duration); - return Unit.INSTANCE; - }); + MuteAccountDialog.showMuteAccountDialog( + this.getActivity(), + accountUsername, + (notifications, duration) -> { + timelineCases.getValue().mute(accountId, notifications, duration); + return Unit.INSTANCE; + }); } private void onBlock(String accountId, String accountUsername) { - new AlertDialog.Builder(requireContext()).setMessage( - getString(R.string.dialog_block_warning, accountUsername)) - .setPositiveButton(android.R.string.ok, - (__, ___) -> timelineCases.getValue().block(accountId)) - .setNegativeButton(android.R.string.cancel, null).show(); + new AlertDialog.Builder(requireContext()) + .setMessage(getString(R.string.dialog_block_warning, accountUsername)) + .setPositiveButton( + android.R.string.ok, (__, ___) -> timelineCases.getValue().block(accountId)) + .setNegativeButton(android.R.string.cancel, null) + .show(); } private static boolean accountIsInMentions(AccountEntity account, Status.Mention[] mentions) { - if(account == null) { + if (account == null) { return false; } - for(Status.Mention mention : mentions) { - if(account.getUsername().equals(mention.getUsername())) { + for (Status.Mention mention : mentions) { + if (account.getUsername().equals(mention.getUsername())) { Uri uri = Uri.parse(mention.getUrl()); - if(uri != null && account.getDomain().equals(uri.getHost())) { + if (uri != null && account.getDomain().equals(uri.getHost())) { return true; } } @@ -411,31 +456,34 @@ public abstract class SFragment extends BaseFragment { final Status actionable = status.getActionableStatus(); final Attachment active = actionable.getAttachments().get(urlIndex); Attachment.Type type = active.getType(); - switch(type) { + switch (type) { case GIFV: case VIDEO: case IMAGE: - case AUDIO: { - final List<AttachmentViewData> attachments = AttachmentViewData.list(actionable); - final Intent intent = - ViewMediaActivity.newIntent(getContext(), attachments, urlIndex); - if(view != null) { - String url = active.getUrl(); - ViewCompat.setTransitionName(view, url); - ActivityOptionsCompat options = - ActivityOptionsCompat.makeSceneTransitionAnimation(getActivity(), view, - url); - startActivity(intent, options.toBundle()); - } else { - startActivity(intent); + case AUDIO: + { + final List<AttachmentViewData> attachments = + AttachmentViewData.list(actionable); + final Intent intent = + ViewMediaActivity.newIntent(getContext(), attachments, urlIndex); + if (view != null) { + String url = active.getUrl(); + ViewCompat.setTransitionName(view, url); + ActivityOptionsCompat options = + ActivityOptionsCompat.makeSceneTransitionAnimation( + getActivity(), view, url); + startActivity(intent, options.toBundle()); + } else { + startActivity(intent); + } + break; } - break; - } default: - case UNKNOWN: { - LinkHelper.openLink(active.getUrl(), getContext()); - break; - } + case UNKNOWN: + { + LinkHelper.openLink(active.getUrl(), getContext()); + break; + } } } @@ -447,60 +495,96 @@ public abstract class SFragment extends BaseFragment { protected void openReportPage(String accountId, String accountUsername, String statusId) { startActivity( - ReportActivity.getIntent(requireContext(), accountId, accountUsername, statusId)); + ReportActivity.getIntent(requireContext(), accountId, accountUsername, statusId)); } protected void showConfirmDeleteDialog(final String id, final int position) { - new AlertDialog.Builder(getActivity()).setMessage(R.string.dialog_delete_toot_warning) - .setPositiveButton(android.R.string.ok, (dialogInterface, i) -> { - timelineCases.getValue().delete(id).observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) - .subscribe(deletedStatus -> { - }, error -> { - Log.w("SFragment", "error deleting status", error); - Toast.makeText(getContext(), R.string.error_generic, Toast.LENGTH_SHORT) - .show(); - }); - removeItem(position); - }).setNegativeButton(android.R.string.cancel, null).show(); + new AlertDialog.Builder(getActivity()) + .setMessage(R.string.dialog_delete_toot_warning) + .setPositiveButton( + android.R.string.ok, + (dialogInterface, i) -> { + timelineCases + .getValue() + .delete(id) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + deletedStatus -> {}, + error -> { + Log.w("SFragment", "error deleting status", error); + Toast.makeText( + getContext(), + R.string.error_generic, + Toast.LENGTH_SHORT) + .show(); + }); + removeItem(position); + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); } private void showConfirmEditDialog(final String id, final int position, final Status status) { - if(getActivity() == null) { + if (getActivity() == null) { return; } - new AlertDialog.Builder(getActivity()).setMessage(R.string.dialog_redraft_toot_warning) - .setPositiveButton(android.R.string.ok, (dialogInterface, i) -> { - timelineCases.getValue().delete(id).observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) - .subscribe(deletedStatus -> { - removeItem(position); - - if(deletedStatus.isEmpty()) { - deletedStatus = status.toDeletedStatus(); - } - ComposeOptions composeOptions = new ComposeOptions(); - composeOptions.setTootText(deletedStatus.getText()); - composeOptions.setInReplyToId(deletedStatus.getInReplyToId()); - composeOptions.setVisibility(deletedStatus.getVisibility()); - 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())); - } - - Intent intent = ComposeActivity.startIntent(getContext(), composeOptions); - startActivity(intent); - }, error -> { - Log.w("SFragment", "error deleting status", error); - Toast.makeText(getContext(), R.string.error_generic, Toast.LENGTH_SHORT) - .show(); - }); - - }).setNegativeButton(android.R.string.cancel, null).show(); + new AlertDialog.Builder(getActivity()) + .setMessage(R.string.dialog_redraft_toot_warning) + .setPositiveButton( + android.R.string.ok, + (dialogInterface, i) -> { + timelineCases + .getValue() + .delete(id) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + deletedStatus -> { + removeItem(position); + + if (deletedStatus.isEmpty()) { + deletedStatus = status.toDeletedStatus(); + } + ComposeOptions composeOptions = + new ComposeOptions(); + composeOptions.setTootText(deletedStatus.getText()); + composeOptions.setInReplyToId( + deletedStatus.getInReplyToId()); + composeOptions.setVisibility( + deletedStatus.getVisibility()); + 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())); + } + + Intent intent = + ComposeActivity.startIntent( + getContext(), composeOptions); + startActivity(intent); + }, + error -> { + Log.w("SFragment", "error deleting status", error); + Toast.makeText( + getContext(), + R.string.error_generic, + Toast.LENGTH_SHORT) + .show(); + }); + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); } private void openAsAccount(String statusUrl, AccountEntity account) { @@ -514,19 +598,19 @@ public abstract class SFragment extends BaseFragment { private void showOpenAsDialog(String statusUrl, CharSequence dialogTitle) { BaseActivity activity = (BaseActivity) getActivity(); - activity.showAccountChooserDialog(dialogTitle, false, - account -> openAsAccount(statusUrl, account)); + activity.showAccountChooserDialog( + dialogTitle, false, account -> openAsAccount(statusUrl, account)); } private void downloadAllMedia(Status status) { Toast.makeText(getContext(), R.string.downloading_media, Toast.LENGTH_SHORT).show(); - for(Attachment attachment : status.getAttachments()) { + for (Attachment attachment : status.getAttachments()) { String url = attachment.getUrl(); Uri uri = Uri.parse(url); String filename = uri.getLastPathSegment(); DownloadManager downloadManager = - (DownloadManager) getActivity().getSystemService(Context.DOWNLOAD_SERVICE); + (DownloadManager) getActivity().getSystemService(Context.DOWNLOAD_SERVICE); DownloadManager.Request request = new DownloadManager.Request(uri); request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename); downloadManager.enqueue(request); @@ -534,17 +618,22 @@ public abstract class SFragment extends BaseFragment { } private void requestDownloadAllMedia(Status status) { - String[] permissions = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}; - ((BaseActivity) getActivity()).requestPermissions(permissions, - (permissions1, grantResults) -> { - if(grantResults.length > 0 && - grantResults[0] == PackageManager.PERMISSION_GRANTED) { - downloadAllMedia(status); - } else { - Toast.makeText(getContext(), R.string.error_media_download_permission, - Toast.LENGTH_SHORT).show(); - } - }); + String[] permissions = new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}; + ((BaseActivity) getActivity()) + .requestPermissions( + permissions, + (permissions1, grantResults) -> { + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + downloadAllMedia(status); + } else { + Toast.makeText( + getContext(), + R.string.error_media_download_permission, + Toast.LENGTH_SHORT) + .show(); + } + }); } public boolean isFilteringMuted() { @@ -554,39 +643,44 @@ public abstract class SFragment extends BaseFragment { public void updateMuteFilter(@NonNull SharedPreferences pref, boolean reload) { filterMuted = pref.getBoolean(PrefKeys.HIDE_MUTED_USERS, false); - if(reload) { + if (reload) { refreshAfterApplyingFilters(); } } public void reloadFilters(SharedPreferences pref, boolean forceRefresh) { - if(pref != null) { + if (pref != null) { updateMuteFilter(pref, false); // will be reloaded later } - if(filters != null && !forceRefresh) { + if (filters != null && !forceRefresh) { applyFilters(forceRefresh); return; } - mastodonApi.getValue().getFilters().enqueue(new Callback<List<Filter>>() { - @Override - public void onResponse(@NonNull Call<List<Filter>> call, - @NonNull Response<List<Filter>> response) - { - filters = response.body(); - if(response.isSuccessful() && filters != null) { - applyFilters(forceRefresh); - } else { - Log.e(TAG, "Error getting filters from server"); - } - } - - @Override - public void onFailure(@NonNull Call<List<Filter>> call, @NonNull Throwable t) { - Log.e(TAG, "Error getting filters from server", t); - } - }); + mastodonApi + .getValue() + .getFilters() + .enqueue( + new Callback<List<Filter>>() { + @Override + public void onResponse( + @NonNull Call<List<Filter>> call, + @NonNull Response<List<Filter>> response) { + filters = response.body(); + if (response.isSuccessful() && filters != null) { + applyFilters(forceRefresh); + } else { + Log.e(TAG, "Error getting filters from server"); + } + } + + @Override + public void onFailure( + @NonNull Call<List<Filter>> call, @NonNull Throwable t) { + Log.e(TAG, "Error getting filters from server", t); + } + }); } protected boolean filterIsRelevant(@NonNull Filter filter) { @@ -602,38 +696,40 @@ public abstract class SFragment extends BaseFragment { @VisibleForTesting public boolean shouldFilterStatus(Status status) { - if(filterMuted && status.getMuted()) { + if (filterMuted && status.getMuted()) { return true; } - if(filterRemoveRegex && status.getPoll() != null) { - for(PollOption option : status.getPoll().getOptions()) { - if(filterRemoveRegexMatcher.reset(option.getTitle()).find()) { + if (filterRemoveRegex && status.getPoll() != null) { + for (PollOption option : status.getPoll().getOptions()) { + if (filterRemoveRegexMatcher.reset(option.getTitle()).find()) { return true; } } } - return (filterRemoveRegex && - (filterRemoveRegexMatcher.reset(status.getActionableStatus().getContent()).find() || - (!status.getSpoilerText().isEmpty() && - filterRemoveRegexMatcher.reset(status.getActionableStatus().getSpoilerText()) - .find()))); + return (filterRemoveRegex + && (filterRemoveRegexMatcher.reset(status.getActionableStatus().getContent()).find() + || (!status.getSpoilerText().isEmpty() + && filterRemoveRegexMatcher + .reset(status.getActionableStatus().getSpoilerText()) + .find()))); } public void applyFilters(boolean refresh) { List<String> tokens = new ArrayList<>(); - for(Filter filter : filters) { - if(filterIsRelevant(filter)) { + for (Filter filter : filters) { + if (filterIsRelevant(filter)) { tokens.add(filterToRegexToken(filter)); } } filterRemoveRegex = !tokens.isEmpty(); - if(filterRemoveRegex) { + if (filterRemoveRegex) { filterRemoveRegexMatcher = - Pattern.compile(TextUtils.join("|", tokens), Pattern.CASE_INSENSITIVE).matcher(""); + Pattern.compile(TextUtils.join("|", tokens), Pattern.CASE_INSENSITIVE) + .matcher(""); } - if(refresh) { + if (refresh) { refreshAfterApplyingFilters(); } } @@ -641,9 +737,11 @@ public abstract class SFragment extends BaseFragment { private static String filterToRegexToken(Filter filter) { String phrase = filter.getPhrase(); String quotedPhrase = Pattern.quote(phrase); - return (filter.getWholeWord() && alphanumeric.reset(phrase).matches()) ? - // "whole word" should only apply to alphanumeric filters, #1543 - String.format("(^|\\W)%s($|\\W)", quotedPhrase) : quotedPhrase; + return (filter.getWholeWord() && alphanumeric.reset(phrase).matches()) + ? + // "whole word" should only apply to alphanumeric filters, #1543 + String.format("(^|\\W)%s($|\\W)", quotedPhrase) + : quotedPhrase; } public static void flushFilters() { diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/fragment/TimePickerFragment.java b/husky/app/src/main/java/com/keylesspalace/tusky/fragment/TimePickerFragment.java index 1349a59..5663da2 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/fragment/TimePickerFragment.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/fragment/TimePickerFragment.java @@ -18,12 +18,9 @@ package com.keylesspalace.tusky.fragment; import android.app.Dialog; import android.app.TimePickerDialog; import android.os.Bundle; - import androidx.annotation.NonNull; import androidx.fragment.app.DialogFragment; - import com.keylesspalace.tusky.components.compose.ComposeActivity; - import java.util.Calendar; import java.util.TimeZone; @@ -42,12 +39,12 @@ public class TimePickerFragment extends DialogFragment { calendar.set(Calendar.MINUTE, args.getInt(PICKER_TIME_MINUTE)); } - return new TimePickerDialog(getContext(), + return new TimePickerDialog( + getContext(), android.R.style.Theme_DeviceDefault_Dialog, (ComposeActivity) getActivity(), calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE), true); } - } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java b/husky/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java index b2310e1..0f6a64d 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java @@ -20,6 +20,10 @@ package com.keylesspalace.tusky.fragment; +import static com.uber.autodispose.AutoDispose.autoDisposable; +import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from; +import static org.koin.java.KoinJavaComponent.inject; + import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -90,8 +94,6 @@ import com.keylesspalace.tusky.util.ViewDataUtils; import com.keylesspalace.tusky.view.BackgroundMessageView; import com.keylesspalace.tusky.view.EndlessOnScrollListener; import com.keylesspalace.tusky.viewdata.StatusViewData; -import static com.uber.autodispose.AutoDispose.autoDisposable; -import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import java.io.IOException; @@ -105,16 +107,16 @@ import java.util.concurrent.TimeUnit; import kotlin.Unit; import kotlin.collections.CollectionsKt; import kotlin.jvm.functions.Function1; -import static org.koin.java.KoinJavaComponent.inject; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import timber.log.Timber; public class TimelineFragment extends SFragment - implements SwipeRefreshLayout.OnRefreshListener, StatusActionListener, ReselectableFragment, - RefreshableFragment -{ + implements SwipeRefreshLayout.OnRefreshListener, + StatusActionListener, + ReselectableFragment, + RefreshableFragment { private static final String KIND_ARG = "kind"; private static final String ID_ARG = "id"; @@ -126,17 +128,28 @@ public class TimelineFragment extends SFragment private boolean isNeedRefresh; public enum Kind { - HOME, PUBLIC_LOCAL, PUBLIC_FEDERATED, PUBLIC_BUBBLE, TAG, USER, USER_PINNED, - USER_WITH_REPLIES, FAVOURITES, LIST, BOOKMARKS + HOME, + PUBLIC_LOCAL, + PUBLIC_FEDERATED, + PUBLIC_BUBBLE, + TAG, + USER, + USER_PINNED, + USER_WITH_REPLIES, + FAVOURITES, + LIST, + BOOKMARKS } private enum FetchEnd { - TOP, BOTTOM, MIDDLE + TOP, + BOTTOM, + MIDDLE } private EventHub eventHub = (EventHub) inject(EventHub.class).getValue(); private TimelineRepository timelineRepo = - (TimelineRepository) inject(TimelineRepository.class).getValue(); + (TimelineRepository) inject(TimelineRepository.class).getValue(); private boolean eventRegistered = false; @@ -150,10 +163,9 @@ public class TimelineFragment extends SFragment private Kind kind; private String id; private List<String> tags; - /** - * For some timeline kinds we must use LINK headers and not just status ids. - */ + /** For some timeline kinds we must use LINK headers and not just status ids. */ private String nextId; + private LinearLayoutManager layoutManager; private EndlessOnScrollListener scrollListener; private boolean filterRemoveReplies; @@ -167,19 +179,20 @@ public class TimelineFragment extends SFragment private boolean initialUpdateFailed = false; private PairedList<Either<Placeholder, Status>, StatusViewData> statuses = - new PairedList<>(new Function<>() { - @Override - public StatusViewData apply(Either<Placeholder, Status> input) { - Status status = input.asRightOrNull(); - if(status != null) { - return ViewDataUtils.statusToViewData(status, alwaysShowSensitiveMedia, - alwaysOpenSpoiler); - } else { - Placeholder placeholder = input.asLeft(); - return new StatusViewData.Placeholder(placeholder.getId(), false); - } - } - }); + new PairedList<>( + new Function<>() { + @Override + public StatusViewData apply(Either<Placeholder, Status> input) { + Status status = input.asRightOrNull(); + if (status != null) { + return ViewDataUtils.statusToViewData( + status, alwaysShowSensitiveMedia, alwaysOpenSpoiler); + } else { + Placeholder placeholder = input.asLeft(); + return new StatusViewData.Placeholder(placeholder.getId(), false); + } + } + }); public static TimelineFragment newInstance(Kind kind) { return newInstance(kind, null); @@ -189,9 +202,8 @@ public class TimelineFragment extends SFragment return newInstance(kind, hashtagOrId, true); } - public static TimelineFragment newInstance(Kind kind, @Nullable String hashtagOrId, - boolean enableSwipeToRefresh) - { + public static TimelineFragment newInstance( + Kind kind, @Nullable String hashtagOrId, boolean enableSwipeToRefresh) { TimelineFragment fragment = new TimelineFragment(); Bundle arguments = new Bundle(3); arguments.putString(KIND_ARG, kind.name()); @@ -216,35 +228,39 @@ public class TimelineFragment extends SFragment super.onCreate(savedInstanceState); Bundle arguments = Objects.requireNonNull(getArguments()); kind = Kind.valueOf(arguments.getString(KIND_ARG)); - if(kind == Kind.USER || kind == Kind.USER_PINNED || kind == Kind.USER_WITH_REPLIES || - kind == Kind.LIST) { + if (kind == Kind.USER + || kind == Kind.USER_PINNED + || kind == Kind.USER_WITH_REPLIES + || kind == Kind.LIST) { id = arguments.getString(ID_ARG); } - if(kind == Kind.TAG) { + if (kind == Kind.TAG) { tags = arguments.getStringArrayList(HASHTAGS_ARG); } SharedPreferences preferences = - PreferenceManager.getDefaultSharedPreferences(getActivity()); + PreferenceManager.getDefaultSharedPreferences(getActivity()); StatusDisplayOptions statusDisplayOptions = - new StatusDisplayOptions(preferences.getBoolean("animateGifAvatars", false), - accountManager.getValue().getActiveAccount().getMediaPreviewEnabled(), - preferences.getBoolean("absoluteTimeView", false), - preferences.getBoolean("showBotOverlay", true), - preferences.getBoolean("useBlurhash", true), - preferences.getBoolean("showCardsInTimelines", false) ? CardViewMode.INDENTED : - CardViewMode.NONE, preferences.getBoolean("confirmReblogs", true), - preferences.getBoolean(PrefKeys.RENDER_STATUS_AS_MENTION, true), - preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false)); + new StatusDisplayOptions( + preferences.getBoolean("animateGifAvatars", false), + accountManager.getValue().getActiveAccount().getMediaPreviewEnabled(), + preferences.getBoolean("absoluteTimeView", false), + preferences.getBoolean("showBotOverlay", true), + preferences.getBoolean("useBlurhash", true), + preferences.getBoolean("showCardsInTimelines", false) + ? CardViewMode.INDENTED + : CardViewMode.NONE, + preferences.getBoolean("confirmReblogs", true), + preferences.getBoolean(PrefKeys.RENDER_STATUS_AS_MENTION, true), + preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false)); adapter = new TimelineAdapter(dataSource, statusDisplayOptions, this); isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true); } @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) - { + public View onCreateView( + @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View rootView = inflater.inflate(R.layout.fragment_timeline, container, false); recyclerView = rootView.findViewById(R.id.recyclerView); @@ -258,13 +274,13 @@ public class TimelineFragment extends SFragment updateAdapter(); setupTimelinePreferences(); - if(statuses.isEmpty()) { + if (statuses.isEmpty()) { progressBar.setVisibility(View.VISIBLE); bottomLoading = true; this.sendInitialRequest(); } else { progressBar.setVisibility(View.GONE); - if(isNeedRefresh) { + if (isNeedRefresh) { onRefresh(); } } @@ -273,7 +289,7 @@ public class TimelineFragment extends SFragment } private void sendInitialRequest() { - if(this.kind == Kind.HOME) { + if (this.kind == Kind.HOME) { this.tryCache(); } else { sendFetchTimelineRequest(null, null, null, FetchEnd.BOTTOM, -1); @@ -283,84 +299,92 @@ public class TimelineFragment extends SFragment private void tryCache() { // Request timeline from disk to make it quick, then replace it with timeline from // the server to update it - timelineRepo.getStatuses(null, null, null, LOAD_AT_ONCE, TimelineRequestMode.DISK) - .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))).subscribe(statuses -> { - filterStatuses(statuses); - - if(statuses.size() > 1) { - this.clearPlaceholdersForResponse(statuses); - this.statuses.clear(); - this.statuses.addAll(statuses); - this.updateAdapter(); - this.progressBar.setVisibility(View.GONE); - // Request statuses including current top to refresh all of them - } + timelineRepo + .getStatuses(null, null, null, LOAD_AT_ONCE, TimelineRequestMode.DISK) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + statuses -> { + filterStatuses(statuses); + + if (statuses.size() > 1) { + this.clearPlaceholdersForResponse(statuses); + this.statuses.clear(); + this.statuses.addAll(statuses); + this.updateAdapter(); + this.progressBar.setVisibility(View.GONE); + // Request statuses including current top to refresh all of them + } - this.updateCurrent(); - this.loadAbove(); - }, throwable -> { - this.updateCurrent(); - this.loadAbove(); - }); + this.updateCurrent(); + this.loadAbove(); + }, + throwable -> { + this.updateCurrent(); + this.loadAbove(); + }); } private void updateCurrent() { - if(this.statuses.isEmpty()) { + if (this.statuses.isEmpty()) { return; } String topId = CollectionsKt.first(this.statuses, Either::isRight).asRight().getId(); - this.timelineRepo.getStatuses(topId, null, null, LOAD_AT_ONCE, TimelineRequestMode.NETWORK) - .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))).subscribe((statuses) -> { - this.initialUpdateFailed = false; - // When cached timeline is too old, we would replace it with nothing - if(!statuses.isEmpty()) { - filterStatuses(statuses); - - if(!this.statuses.isEmpty()) { - // clear old cached statuses - Iterator<Either<Placeholder, Status>> iterator = this.statuses.iterator(); - while(iterator.hasNext()) { - Either<Placeholder, Status> item = iterator.next(); - if(item.isRight()) { - Status status = item.asRight(); - if(status.getId().length() < topId.length() || - status.getId().compareTo(topId) < 0) { - - iterator.remove(); + this.timelineRepo + .getStatuses(topId, null, null, LOAD_AT_ONCE, TimelineRequestMode.NETWORK) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + (statuses) -> { + this.initialUpdateFailed = false; + // When cached timeline is too old, we would replace it with nothing + if (!statuses.isEmpty()) { + filterStatuses(statuses); + + if (!this.statuses.isEmpty()) { + // clear old cached statuses + Iterator<Either<Placeholder, Status>> iterator = + this.statuses.iterator(); + while (iterator.hasNext()) { + Either<Placeholder, Status> item = iterator.next(); + if (item.isRight()) { + Status status = item.asRight(); + if (status.getId().length() < topId.length() + || status.getId().compareTo(topId) < 0) { + + iterator.remove(); + } + } else { + Placeholder placeholder = item.asLeft(); + if (placeholder.getId().length() < topId.length() + || placeholder.getId().compareTo(topId) < 0) { + + iterator.remove(); + } + } + } } - } else { - Placeholder placeholder = item.asLeft(); - if(placeholder.getId().length() < topId.length() || - placeholder.getId().compareTo(topId) < 0) { - iterator.remove(); - } + this.statuses.addAll(statuses); + this.updateAdapter(); } - - } - } - - this.statuses.addAll(statuses); - this.updateAdapter(); - } - this.bottomLoading = false; - this.progressBar.setVisibility(View.GONE); - this.swipeRefreshLayout.setRefreshing(false); - }, (e) -> { - this.initialUpdateFailed = true; - // Indicate that we are not loading anymore - this.progressBar.setVisibility(View.GONE); - this.swipeRefreshLayout.setRefreshing(false); - }); + this.bottomLoading = false; + this.progressBar.setVisibility(View.GONE); + this.swipeRefreshLayout.setRefreshing(false); + }, + (e) -> { + this.initialUpdateFailed = true; + // Indicate that we are not loading anymore + this.progressBar.setVisibility(View.GONE); + this.swipeRefreshLayout.setRefreshing(false); + }); } private void setupTimelinePreferences() { alwaysShowSensitiveMedia = - accountManager.getValue().getActiveAccount().getAlwaysShowSensitiveMedia(); + accountManager.getValue().getActiveAccount().getAlwaysShowSensitiveMedia(); alwaysOpenSpoiler = accountManager.getValue().getActiveAccount().getAlwaysOpenSpoiler(); SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext()); @@ -375,7 +399,7 @@ public class TimelineFragment extends SFragment private static boolean filterContextMatchesKind(Kind kind, List<String> filterContext) { // home, notifications, public, thread - switch(kind) { + switch (kind) { case HOME: case LIST: return filterContext.contains(Filter.HOME); @@ -385,8 +409,8 @@ public class TimelineFragment extends SFragment case TAG: return filterContext.contains(Filter.PUBLIC); case FAVOURITES: - return (filterContext.contains(Filter.PUBLIC) || - filterContext.contains(Filter.NOTIFICATIONS)); + return (filterContext.contains(Filter.PUBLIC) + || filterContext.contains(Filter.NOTIFICATIONS)); case USER: case USER_WITH_REPLIES: case USER_PINNED: @@ -408,7 +432,7 @@ public class TimelineFragment extends SFragment private void setupSwipeRefreshLayout() { swipeRefreshLayout.setEnabled(isSwipeToRefreshEnabled); - if(isSwipeToRefreshEnabled) { + if (isSwipeToRefreshEnabled) { swipeRefreshLayout.setOnRefreshListener(this); swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue); } @@ -416,13 +440,14 @@ public class TimelineFragment extends SFragment private void setupRecyclerView() { recyclerView.setAccessibilityDelegateCompat( - new ListStatusAccessibilityDelegate(recyclerView, this, statuses::getPairedItemOrNull)); + new ListStatusAccessibilityDelegate( + recyclerView, this, statuses::getPairedItemOrNull)); Context context = recyclerView.getContext(); recyclerView.setHasFixedSize(true); layoutManager = new LinearLayoutManager(context); recyclerView.setLayoutManager(layoutManager); DividerItemDecoration divider = - new DividerItemDecoration(context, layoutManager.getOrientation()); + new DividerItemDecoration(context, layoutManager.getOrientation()); recyclerView.addItemDecoration(divider); // CWs are expanded without animation, buttons animate itself, we don't need it basically @@ -432,15 +457,15 @@ public class TimelineFragment extends SFragment } private void deleteStatusById(String id) { - for(int i = 0; i < statuses.size(); i++) { + for (int i = 0; i < statuses.size(); i++) { Either<Placeholder, Status> either = statuses.get(i); - if(either.isRight() && id.equals(either.asRight().getId())) { + if (either.isRight() && id.equals(either.asRight().getId())) { statuses.remove(either); updateAdapter(); break; } } - if(statuses.size() == 0) { + if (statuses.size() == 0) { showNothing(); } } @@ -455,139 +480,150 @@ public class TimelineFragment extends SFragment super.onActivityCreated(savedInstanceState); SharedPreferences preferences = - PreferenceManager.getDefaultSharedPreferences(requireContext()); + PreferenceManager.getDefaultSharedPreferences(requireContext()); /* This is delayed until onActivityCreated solely because MainActivity.composeButton isn't * guaranteed to be set until then. */ - if(actionButtonPresent()) { + if (actionButtonPresent()) { /* Use a modified scroll listener that both loads more statuses as it goes, and hides * the follow button on down-scroll. */ hideFab = preferences.getBoolean("fabHide", false); - scrollListener = new EndlessOnScrollListener(layoutManager) { - @Override - public void onScrolled(RecyclerView view, int dx, int dy) { - super.onScrolled(view, dx, dy); - - ActionButtonActivity activity = (ActionButtonActivity) getActivity(); - FloatingActionButton composeButton = activity.getActionButton(); - - if(composeButton != null) { - if(hideFab) { - if(dy > 0 && composeButton.isShown()) { - composeButton.hide(); // hides the button if we're scrolling down - activity.onActionButtonHidden(); - } else if(dy < 0 && !composeButton.isShown()) { - composeButton.show(); // shows it if we are scrolling up + scrollListener = + new EndlessOnScrollListener(layoutManager) { + @Override + public void onScrolled(RecyclerView view, int dx, int dy) { + super.onScrolled(view, dx, dy); + + ActionButtonActivity activity = (ActionButtonActivity) getActivity(); + FloatingActionButton composeButton = activity.getActionButton(); + + if (composeButton != null) { + if (hideFab) { + if (dy > 0 && composeButton.isShown()) { + composeButton + .hide(); // hides the button if we're scrolling down + activity.onActionButtonHidden(); + } else if (dy < 0 && !composeButton.isShown()) { + composeButton.show(); // shows it if we are scrolling up + } + } else if (!composeButton.isShown()) { + composeButton.show(); + } } - } else if(!composeButton.isShown()) { - composeButton.show(); } - } - } - @Override - public void onLoadMore(int totalItemsCount, RecyclerView view) { - TimelineFragment.this.onLoadMore(); - } - }; + @Override + public void onLoadMore(int totalItemsCount, RecyclerView view) { + TimelineFragment.this.onLoadMore(); + } + }; } else { // Just use the basic scroll listener to load more statuses. - scrollListener = new EndlessOnScrollListener(layoutManager) { - @Override - public void onLoadMore(int totalItemsCount, RecyclerView view) { - TimelineFragment.this.onLoadMore(); - } - }; + scrollListener = + new EndlessOnScrollListener(layoutManager) { + @Override + public void onLoadMore(int totalItemsCount, RecyclerView view) { + TimelineFragment.this.onLoadMore(); + } + }; } recyclerView.addOnScrollListener(scrollListener); - if(!eventRegistered) { - eventHub.getEvents().observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))).subscribe(event -> { - if(event instanceof FavoriteEvent) { - FavoriteEvent favEvent = ((FavoriteEvent) event); - handleFavEvent(favEvent); - } else if(event instanceof ReblogEvent) { - ReblogEvent reblogEvent = (ReblogEvent) event; - handleReblogEvent(reblogEvent); - } else if(event instanceof BookmarkEvent) { - BookmarkEvent bookmarkEvent = (BookmarkEvent) event; - handleBookmarkEvent(bookmarkEvent); - } else if(event instanceof UnfollowEvent) { - if(kind == Kind.HOME) { - String id = ((UnfollowEvent) event).getAccountId(); - removeAllByAccountId(id); - } - } else if(event instanceof BlockEvent) { - if(kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && - kind != Kind.USER_PINNED) { - String id = ((BlockEvent) event).getAccountId(); - removeAllByAccountId(id); - } - } else if(event instanceof MuteConversationEvent) { - if(kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && - kind != Kind.USER_PINNED) { - handleMuteStatusEvent((MuteConversationEvent) event); - } - } else if(event instanceof MuteEvent) { - if(kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && - kind != Kind.USER_PINNED) { - handleMuteEvent((MuteEvent) event); - } - } else if(event instanceof DomainMuteEvent) { - if(kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && - kind != Kind.USER_PINNED) { - String instance = ((DomainMuteEvent) event).getInstance(); - removeAllByInstance(instance); - } - } else if(event instanceof StatusDeletedEvent) { - if(kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && - kind != Kind.USER_PINNED) { - String id = ((StatusDeletedEvent) event).getStatusId(); - deleteStatusById(id); - } - } else if(event instanceof StatusComposedEvent) { - Status status = ((StatusComposedEvent) event).getStatus(); - handleStatusComposeEvent(status); - } else if(event instanceof PreferenceChangedEvent) { - onPreferenceChanged(((PreferenceChangedEvent) event).getPreferenceKey()); - } else if(event instanceof EmojiReactEvent) { - handleEmojiReactEvent((EmojiReactEvent) event); - } - }); + if (!eventRegistered) { + eventHub.getEvents() + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + event -> { + if (event instanceof FavoriteEvent) { + FavoriteEvent favEvent = ((FavoriteEvent) event); + handleFavEvent(favEvent); + } else if (event instanceof ReblogEvent) { + ReblogEvent reblogEvent = (ReblogEvent) event; + handleReblogEvent(reblogEvent); + } else if (event instanceof BookmarkEvent) { + BookmarkEvent bookmarkEvent = (BookmarkEvent) event; + handleBookmarkEvent(bookmarkEvent); + } else if (event instanceof UnfollowEvent) { + if (kind == Kind.HOME) { + String id = ((UnfollowEvent) event).getAccountId(); + removeAllByAccountId(id); + } + } else if (event instanceof BlockEvent) { + if (kind != Kind.USER + && kind != Kind.USER_WITH_REPLIES + && kind != Kind.USER_PINNED) { + String id = ((BlockEvent) event).getAccountId(); + removeAllByAccountId(id); + } + } else if (event instanceof MuteConversationEvent) { + if (kind != Kind.USER + && kind != Kind.USER_WITH_REPLIES + && kind != Kind.USER_PINNED) { + handleMuteStatusEvent((MuteConversationEvent) event); + } + } else if (event instanceof MuteEvent) { + if (kind != Kind.USER + && kind != Kind.USER_WITH_REPLIES + && kind != Kind.USER_PINNED) { + handleMuteEvent((MuteEvent) event); + } + } else if (event instanceof DomainMuteEvent) { + if (kind != Kind.USER + && kind != Kind.USER_WITH_REPLIES + && kind != Kind.USER_PINNED) { + String instance = ((DomainMuteEvent) event).getInstance(); + removeAllByInstance(instance); + } + } else if (event instanceof StatusDeletedEvent) { + if (kind != Kind.USER + && kind != Kind.USER_WITH_REPLIES + && kind != Kind.USER_PINNED) { + String id = ((StatusDeletedEvent) event).getStatusId(); + deleteStatusById(id); + } + } else if (event instanceof StatusComposedEvent) { + Status status = ((StatusComposedEvent) event).getStatus(); + handleStatusComposeEvent(status); + } else if (event instanceof PreferenceChangedEvent) { + onPreferenceChanged( + ((PreferenceChangedEvent) event).getPreferenceKey()); + } else if (event instanceof EmojiReactEvent) { + handleEmojiReactEvent((EmojiReactEvent) event); + } + }); eventRegistered = true; } } @Override public void onRefresh() { - if(isSwipeToRefreshEnabled) { + if (isSwipeToRefreshEnabled) { swipeRefreshLayout.setEnabled(true); } this.statusView.setVisibility(View.GONE); isNeedRefresh = false; - if(this.initialUpdateFailed) { + if (this.initialUpdateFailed) { updateCurrent(); } this.loadAbove(); - } private void loadAbove() { String firstOrNull = null; String secondOrNull = null; - for(int i = 0; i < this.statuses.size(); i++) { + for (int i = 0; i < this.statuses.size(); i++) { Either<Placeholder, Status> status = this.statuses.get(i); - if(status.isRight()) { + if (status.isRight()) { firstOrNull = status.asRight().getId(); - if(i + 1 < statuses.size() && statuses.get(i + 1).isRight()) { + if (i + 1 < statuses.size() && statuses.get(i + 1).isRight()) { secondOrNull = statuses.get(i + 1).asRight().getId(); } break; } } - if(firstOrNull != null) { + if (firstOrNull != null) { this.sendFetchTimelineRequest(null, firstOrNull, secondOrNull, FetchEnd.TOP, -1); } else { this.sendFetchTimelineRequest(null, null, null, FetchEnd.BOTTOM, -1); @@ -602,27 +638,38 @@ public class TimelineFragment extends SFragment @Override public void onReblog(final boolean reblog, final int position) { final Status status = statuses.get(position).asRight(); - timelineCases.getValue().reblog(status, reblog).observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) - .subscribe((newStatus) -> setRebloggedForStatus(position, status, reblog), - (err) -> Timber.e( - "Failed to reblog status " + status.getId() + ", Error[" + err + "]")); + timelineCases + .getValue() + .reblog(status, reblog) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + (newStatus) -> setRebloggedForStatus(position, status, reblog), + (err) -> + Timber.e( + "Failed to reblog status " + + status.getId() + + ", Error[" + + err + + "]")); } private void setRebloggedForStatus(int position, Status status, boolean reblog) { status.setReblogged(reblog); - if(status.getReblog() != null) { + if (status.getReblog() != null) { status.getReblog().setReblogged(reblog); } Pair<StatusViewData.Concrete, Integer> actual = findStatusAndPosition(position, status); - if(actual == null) { + if (actual == null) { return; } StatusViewData newViewData = - new StatusViewData.Builder(actual.first).setReblogged(reblog).createStatusViewData(); + new StatusViewData.Builder(actual.first) + .setReblogged(reblog) + .createStatusViewData(); statuses.setPairedItem(actual.second, newViewData); updateAdapter(); } @@ -630,29 +677,38 @@ public class TimelineFragment extends SFragment @Override public void onFavourite(final boolean favourite, final int position) { final Status status = statuses.get(position).asRight(); - timelineCases.getValue().favourite(status, favourite) - .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) - .subscribe((newStatus) -> setFavouriteForStatus(position, newStatus, favourite), - (err) -> Timber.e( - "Failed to favourite status " + status.getId() + ", Error [" + err + "]")); + timelineCases + .getValue() + .favourite(status, favourite) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + (newStatus) -> setFavouriteForStatus(position, newStatus, favourite), + (err) -> + Timber.e( + "Failed to favourite status " + + status.getId() + + ", Error [" + + err + + "]")); } private void setFavouriteForStatus(int position, Status status, boolean favourite) { status.setFavourited(favourite); - if(status.getReblog() != null) { + if (status.getReblog() != null) { status.getReblog().setFavourited(favourite); } Pair<StatusViewData.Concrete, Integer> actual = findStatusAndPosition(position, status); - if(actual == null) { + if (actual == null) { return; } StatusViewData newViewData = - new StatusViewData.Builder(actual.first).setFavourited(favourite) - .createStatusViewData(); + new StatusViewData.Builder(actual.first) + .setFavourited(favourite) + .createStatusViewData(); statuses.setPairedItem(actual.second, newViewData); updateAdapter(); } @@ -660,47 +716,54 @@ public class TimelineFragment extends SFragment @Override public void onBookmark(final boolean bookmark, final int position) { final Status status = statuses.get(position).asRight(); - timelineCases.getValue().bookmark(status, bookmark) - .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) - .subscribe((newStatus) -> setBookmarkForStatus(position, newStatus, bookmark), - (err) -> Timber.e(err, "Failed to favourite status " + status.getId())); + timelineCases + .getValue() + .bookmark(status, bookmark) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + (newStatus) -> setBookmarkForStatus(position, newStatus, bookmark), + (err) -> Timber.e(err, "Failed to favourite status " + status.getId())); } private void setBookmarkForStatus(int position, Status status, boolean bookmark) { status.setBookmarked(bookmark); - if(status.getReblog() != null) { + if (status.getReblog() != null) { status.getReblog().setBookmarked(bookmark); } Pair<StatusViewData.Concrete, Integer> actual = findStatusAndPosition(position, status); - if(actual == null) { + if (actual == null) { return; } StatusViewData newViewData = - new StatusViewData.Builder(actual.first).setBookmarked(bookmark).createStatusViewData(); + new StatusViewData.Builder(actual.first) + .setBookmarked(bookmark) + .createStatusViewData(); statuses.setPairedItem(actual.second, newViewData); updateAdapter(); } @Override public void onMute(int position, boolean isMuted) { - StatusViewData.Concrete statusViewData = new StatusViewData.Builder( - (StatusViewData.Concrete) statuses.getPairedItem(position)).setMuted(isMuted) - .createStatusViewData(); + StatusViewData.Concrete statusViewData = + new StatusViewData.Builder( + (StatusViewData.Concrete) statuses.getPairedItem(position)) + .setMuted(isMuted) + .createStatusViewData(); statuses.setPairedItem(position, statusViewData); updateAdapter(); } - private void setMutedStatusForStatus(int position, Status status, boolean muted, - boolean threadMuted) - { + private void setMutedStatusForStatus( + int position, Status status, boolean muted, boolean threadMuted) { status.setThreadMuted(threadMuted); StatusViewData.Builder statusViewData = - new StatusViewData.Builder((StatusViewData.Concrete) statuses.getPairedItem(position)); + new StatusViewData.Builder( + (StatusViewData.Concrete) statuses.getPairedItem(position)); statusViewData.setMuted(muted); statusViewData.setThreadMuted(threadMuted); @@ -715,20 +778,24 @@ public class TimelineFragment extends SFragment setVoteForPoll(position, status, votedPoll); - timelineCases.getValue().voteInPoll(status, choices) - .observeOn(AndroidSchedulers.mainThread()).as(autoDisposable(from(this))) - .subscribe((newPoll) -> setVoteForPoll(position, status, newPoll), - (t) -> Timber.e(t, "Failed to vote in poll: " + status.getId())); + timelineCases + .getValue() + .voteInPoll(status, choices) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this))) + .subscribe( + (newPoll) -> setVoteForPoll(position, status, newPoll), + (t) -> Timber.e(t, "Failed to vote in poll: " + status.getId())); } private void setVoteForPoll(int position, Status status, Poll newPoll) { Pair<StatusViewData.Concrete, Integer> actual = findStatusAndPosition(position, status); - if(actual == null) { + if (actual == null) { return; } StatusViewData newViewData = - new StatusViewData.Builder(actual.first).setPoll(newPoll).createStatusViewData(); + new StatusViewData.Builder(actual.first).setPoll(newPoll).createStatusViewData(); statuses.setPairedItem(actual.second, newViewData); updateAdapter(); } @@ -745,29 +812,32 @@ public class TimelineFragment extends SFragment @Override public void onExpandedChange(boolean expanded, int position) { - StatusViewData newViewData = new StatusViewData.Builder( - ((StatusViewData.Concrete) statuses.getPairedItem(position))).setIsExpanded(expanded) - .createStatusViewData(); + StatusViewData newViewData = + new StatusViewData.Builder( + ((StatusViewData.Concrete) statuses.getPairedItem(position))) + .setIsExpanded(expanded) + .createStatusViewData(); statuses.setPairedItem(position, newViewData); updateAdapter(); } @Override public void onContentHiddenChange(boolean isShowing, int position) { - StatusViewData newViewData = new StatusViewData.Builder( - ((StatusViewData.Concrete) statuses.getPairedItem( - position))).setIsShowingSensitiveContent(isShowing).createStatusViewData(); + StatusViewData newViewData = + new StatusViewData.Builder( + ((StatusViewData.Concrete) statuses.getPairedItem(position))) + .setIsShowingSensitiveContent(isShowing) + .createStatusViewData(); statuses.setPairedItem(position, newViewData); updateAdapter(); } - @Override public void onShowReblogs(int position) { String statusId = statuses.get(position).asRight().getId(); Intent intent = - AccountListActivity.newIntent(getContext(), AccountListActivity.Type.REBLOGGED, - statusId); + AccountListActivity.newIntent( + getContext(), AccountListActivity.Type.REBLOGGED, statusId); ((BaseActivity) getActivity()).startActivityWithSlideInAnimation(intent); } @@ -775,26 +845,27 @@ public class TimelineFragment extends SFragment public void onShowFavs(int position) { String statusId = statuses.get(position).asRight().getId(); Intent intent = - AccountListActivity.newIntent(getContext(), AccountListActivity.Type.FAVOURITED, - statusId); + AccountListActivity.newIntent( + getContext(), AccountListActivity.Type.FAVOURITED, statusId); ((BaseActivity) getActivity()).startActivityWithSlideInAnimation(intent); } @Override public void onLoadMore(int position) { - //check bounds before accessing list, - if(statuses.size() >= position && position > 0) { + // check bounds before accessing list, + if (statuses.size() >= position && position > 0) { Status fromStatus = statuses.get(position - 1).asRightOrNull(); Status toStatus = statuses.get(position + 1).asRightOrNull(); String maxMinusOne = - statuses.size() > position + 1 && statuses.get(position + 2).isRight() ? - statuses.get(position + 1).asRight().getId() : null; - if(fromStatus == null || toStatus == null) { + statuses.size() > position + 1 && statuses.get(position + 2).isRight() + ? statuses.get(position + 1).asRight().getId() + : null; + if (fromStatus == null || toStatus == null) { Timber.e("Failed to load more at " + position + ", wrong placeholder position"); return; } - sendFetchTimelineRequest(fromStatus.getId(), toStatus.getId(), maxMinusOne, - FetchEnd.MIDDLE, position); + sendFetchTimelineRequest( + fromStatus.getId(), toStatus.getId(), maxMinusOne, FetchEnd.MIDDLE, position); Placeholder placeholder = statuses.get(position).asLeft(); StatusViewData newViewData = new StatusViewData.Placeholder(placeholder.getId(), true); @@ -807,27 +878,33 @@ public class TimelineFragment extends SFragment @Override public void onContentCollapsedChange(boolean isCollapsed, int position) { - if(position < 0 || position >= statuses.size()) { + if (position < 0 || position >= statuses.size()) { Timber.e( - String.format("Tried to access out of bounds status position: %d of %d", position, - statuses.size() - 1)); + String.format( + "Tried to access out of bounds status position: %d of %d", + position, statuses.size() - 1)); return; } StatusViewData status = statuses.getPairedItem(position); - if(!(status instanceof StatusViewData.Concrete)) { + if (!(status instanceof StatusViewData.Concrete)) { // Statuses PairedList contains a base type of StatusViewData.Concrete and also doesn't - // check for null values when adding values to it although this doesn't seem to be an issue. - Timber.e(String.format( - "Expected StatusViewData.Concrete, got %s instead at position: %d of %d", - status == null ? "<null>" : status.getClass().getSimpleName(), position, - statuses.size() - 1)); + // check for null values when adding values to it although this doesn't seem to be an + // issue. + Timber.e( + String.format( + "Expected StatusViewData.Concrete, got %s instead at position: %d of" + + " %d", + status == null ? "<null>" : status.getClass().getSimpleName(), + position, + statuses.size() - 1)); return; } StatusViewData updatedStatus = - new StatusViewData.Builder((StatusViewData.Concrete) status).setCollapsed(isCollapsed) - .createStatusViewData(); + new StatusViewData.Builder((StatusViewData.Concrete) status) + .setCollapsed(isCollapsed) + .createStatusViewData(); statuses.setPairedItem(position, updatedStatus); updateAdapter(); } @@ -835,7 +912,7 @@ public class TimelineFragment extends SFragment @Override public void onViewMedia(int position, int attachmentIndex, @Nullable View view) { Status status = statuses.get(position).asRightOrNull(); - if(status == null) { + if (status == null) { return; } super.viewMedia(attachmentIndex, status, view); @@ -849,13 +926,15 @@ public class TimelineFragment extends SFragment @Override public void onViewReplyTo(int position) { Status status = statuses.get(position).asRightOrNull(); - if(status == null) { + if (status == null) { return; } - String replyToId = status.getReblog() == null ? status.getInReplyToId() : - status.getReblog().getInReplyToId(); - if(replyToId == null) { + String replyToId = + status.getReblog() == null + ? status.getInReplyToId() + : status.getReblog().getInReplyToId(); + if (replyToId == null) { return; } super.onShowReplyTo(replyToId); @@ -863,7 +942,7 @@ public class TimelineFragment extends SFragment @Override public void onViewTag(String tag) { - if(kind == Kind.TAG && tags.size() == 1 && tags.contains(tag)) { + if (kind == Kind.TAG && tags.size() == 1 && tags.contains(tag)) { // If already viewing a tag page, then ignore any request to view that tag again. return; } @@ -872,7 +951,7 @@ public class TimelineFragment extends SFragment @Override public void onViewAccount(String id) { - if((kind == Kind.USER || kind == Kind.USER_WITH_REPLIES) && this.id.equals(id)) { + if ((kind == Kind.USER || kind == Kind.USER_WITH_REPLIES) && this.id.equals(id)) { /* If already viewing an account page, then any requests to view that account page * should be ignored. */ return; @@ -882,60 +961,70 @@ public class TimelineFragment extends SFragment private void onPreferenceChanged(String key) { SharedPreferences sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(getContext()); - switch(key) { - case "fabHide": { - hideFab = sharedPreferences.getBoolean("fabHide", false); - break; - } - case "mediaPreviewEnabled": { - boolean enabled = - accountManager.getValue().getActiveAccount().getMediaPreviewEnabled(); - boolean oldMediaPreviewEnabled = adapter.getMediaPreviewEnabled(); - if(enabled != oldMediaPreviewEnabled) { - adapter.setMediaPreviewEnabled(enabled); - fullyRefresh(); + PreferenceManager.getDefaultSharedPreferences(getContext()); + switch (key) { + case "fabHide": + { + hideFab = sharedPreferences.getBoolean("fabHide", false); + break; } - break; - } - case "tabFilterHomeReplies": { - boolean filter = sharedPreferences.getBoolean("tabFilterHomeReplies", true); - boolean oldRemoveReplies = filterRemoveReplies; - filterRemoveReplies = kind == Kind.HOME && !filter; - if(adapter.getItemCount() > 1 && oldRemoveReplies != filterRemoveReplies) { - fullyRefresh(); + case "mediaPreviewEnabled": + { + boolean enabled = + accountManager.getValue().getActiveAccount().getMediaPreviewEnabled(); + boolean oldMediaPreviewEnabled = adapter.getMediaPreviewEnabled(); + if (enabled != oldMediaPreviewEnabled) { + adapter.setMediaPreviewEnabled(enabled); + fullyRefresh(); + } + break; } - break; - } - case "tabFilterHomeBoosts": { - boolean filter = sharedPreferences.getBoolean("tabFilterHomeBoosts", true); - boolean oldRemoveReblogs = filterRemoveReblogs; - filterRemoveReblogs = kind == Kind.HOME && !filter; - if(adapter.getItemCount() > 1 && oldRemoveReblogs != filterRemoveReblogs) { - fullyRefresh(); + case "tabFilterHomeReplies": + { + boolean filter = sharedPreferences.getBoolean("tabFilterHomeReplies", true); + boolean oldRemoveReplies = filterRemoveReplies; + filterRemoveReplies = kind == Kind.HOME && !filter; + if (adapter.getItemCount() > 1 && oldRemoveReplies != filterRemoveReplies) { + fullyRefresh(); + } + break; + } + case "tabFilterHomeBoosts": + { + boolean filter = sharedPreferences.getBoolean("tabFilterHomeBoosts", true); + boolean oldRemoveReblogs = filterRemoveReblogs; + filterRemoveReblogs = kind == Kind.HOME && !filter; + if (adapter.getItemCount() > 1 && oldRemoveReblogs != filterRemoveReblogs) { + fullyRefresh(); + } + break; + } + case PrefKeys.HIDE_MUTED_USERS: + { + updateMuteFilter(sharedPreferences, true); + break; } - break; - } - case PrefKeys.HIDE_MUTED_USERS: { - updateMuteFilter(sharedPreferences, true); - break; - } case Filter.HOME: case Filter.NOTIFICATIONS: case Filter.THREAD: case Filter.PUBLIC: - case Filter.ACCOUNT: { - if(filterContextMatchesKind(kind, Collections.singletonList(key))) { - reloadFilters(sharedPreferences, true); + case Filter.ACCOUNT: + { + if (filterContextMatchesKind(kind, Collections.singletonList(key))) { + reloadFilters(sharedPreferences, true); + } + break; + } + case "alwaysShowSensitiveMedia": + { + // it is ok if only newly loaded statuses are affected, no need to fully refresh + alwaysShowSensitiveMedia = + accountManager + .getValue() + .getActiveAccount() + .getAlwaysShowSensitiveMedia(); + break; } - break; - } - case "alwaysShowSensitiveMedia": { - //it is ok if only newly loaded statuses are affected, no need to fully refresh - alwaysShowSensitiveMedia = - accountManager.getValue().getActiveAccount().getAlwaysShowSensitiveMedia(); - break; - } } } @@ -948,10 +1037,12 @@ public class TimelineFragment extends SFragment private void removeAllByConversationId(String conversationId) { // using iterator to safely remove items while iterating Iterator<Either<Placeholder, Status>> iterator = statuses.iterator(); - while(iterator.hasNext()) { + while (iterator.hasNext()) { Status status = iterator.next().asRightOrNull(); - if(status != null && (status.getConversationId().equalsIgnoreCase(conversationId)) || - status.getActionableStatus().getConversationId().equalsIgnoreCase(conversationId)) { + if (status != null && (status.getConversationId().equalsIgnoreCase(conversationId)) + || status.getActionableStatus() + .getConversationId() + .equalsIgnoreCase(conversationId)) { iterator.remove(); } } @@ -961,11 +1052,14 @@ public class TimelineFragment extends SFragment private void removeAllByAccountId(String accountId) { // using iterator to safely remove items while iterating Iterator<Either<Placeholder, Status>> iterator = statuses.iterator(); - while(iterator.hasNext()) { + while (iterator.hasNext()) { Status status = iterator.next().asRightOrNull(); - if(status != null && (status.getAccount().getId().equals(accountId) || - status.getActionableStatus().getAccount().getId() - .equals(accountId))) { + if (status != null + && (status.getAccount().getId().equals(accountId) + || status.getActionableStatus() + .getAccount() + .getId() + .equals(accountId))) { iterator.remove(); } } @@ -975,10 +1069,10 @@ public class TimelineFragment extends SFragment private void removeAllByInstance(String instance) { // using iterator to safely remove items while iterating Iterator<Either<Placeholder, Status>> iterator = statuses.iterator(); - while(iterator.hasNext()) { + while (iterator.hasNext()) { Status status = iterator.next().asRightOrNull(); - if(status != null && - LinkHelper.getDomain(status.getAccount().getUrl()).equals(instance)) { + if (status != null + && LinkHelper.getDomain(status.getAccount().getUrl()).equals(instance)) { iterator.remove(); } } @@ -986,11 +1080,11 @@ public class TimelineFragment extends SFragment } private void onLoadMore() { - if(didLoadEverythingBottom || bottomLoading) { + if (didLoadEverythingBottom || bottomLoading) { return; } - if(statuses.size() == 0) { + if (statuses.size() == 0) { sendInitialRequest(); return; } @@ -999,27 +1093,27 @@ public class TimelineFragment extends SFragment Either<Placeholder, Status> last = statuses.get(statuses.size() - 1); Placeholder placeholder; - if(last.isRight()) { + if (last.isRight()) { final String placeholderId = StringUtils.dec(last.asRight().getId()); placeholder = new Placeholder(placeholderId); statuses.add(new Either.Left<>(placeholder)); } else { placeholder = last.asLeft(); } - statuses.setPairedItem(statuses.size() - 1, - new StatusViewData.Placeholder(placeholder.getId(), true)); + statuses.setPairedItem( + statuses.size() - 1, new StatusViewData.Placeholder(placeholder.getId(), true)); updateAdapter(); String bottomId = null; - if(kind == Kind.FAVOURITES || kind == Kind.BOOKMARKS) { + if (kind == Kind.FAVOURITES || kind == Kind.BOOKMARKS) { bottomId = this.nextId; } else { final ListIterator<Either<Placeholder, Status>> iterator = - this.statuses.listIterator(this.statuses.size()); - while(iterator.hasPrevious()) { + this.statuses.listIterator(this.statuses.size()); + while (iterator.hasPrevious()) { Either<Placeholder, Status> previous = iterator.previous(); - if(previous.isRight()) { + if (previous.isRight()) { bottomId = previous.asRight().getId(); break; } @@ -1036,12 +1130,14 @@ public class TimelineFragment extends SFragment } private boolean actionButtonPresent() { - return kind != Kind.TAG && kind != Kind.FAVOURITES && kind != Kind.BOOKMARKS && - getActivity() instanceof ActionButtonActivity; + return kind != Kind.TAG + && kind != Kind.FAVOURITES + && kind != Kind.BOOKMARKS + && getActivity() instanceof ActionButtonActivity; } private void jumpToTop() { - if(isAdded()) { + if (isAdded()) { layoutManager.scrollToPosition(0); recyclerView.stopScroll(); scrollListener.reset(); @@ -1049,7 +1145,7 @@ public class TimelineFragment extends SFragment } private Call<List<Status>> getFetchCallByTimelineType(String fromId, String uptoId) { - switch(kind) { + switch (kind) { default: case HOME: return mastodonApi.getValue().homeTimeline(fromId, uptoId, LOAD_AT_ONCE); @@ -1062,18 +1158,27 @@ public class TimelineFragment extends SFragment case TAG: String firstHashtag = tags.get(0); List<String> additionalHashtags = tags.subList(1, tags.size()); - return mastodonApi.getValue() - .hashtagTimeline(firstHashtag, additionalHashtags, null, fromId, uptoId, - LOAD_AT_ONCE); + return mastodonApi + .getValue() + .hashtagTimeline( + firstHashtag, + additionalHashtags, + null, + fromId, + uptoId, + LOAD_AT_ONCE); case USER: - return mastodonApi.getValue() - .accountStatuses(id, fromId, uptoId, LOAD_AT_ONCE, true, null, null); + return mastodonApi + .getValue() + .accountStatuses(id, fromId, uptoId, LOAD_AT_ONCE, true, null, null); case USER_PINNED: - return mastodonApi.getValue() - .accountStatuses(id, fromId, uptoId, LOAD_AT_ONCE, null, null, true); + return mastodonApi + .getValue() + .accountStatuses(id, fromId, uptoId, LOAD_AT_ONCE, null, null, true); case USER_WITH_REPLIES: - return mastodonApi.getValue() - .accountStatuses(id, fromId, uptoId, LOAD_AT_ONCE, null, null, null); + return mastodonApi + .getValue() + .accountStatuses(id, fromId, uptoId, LOAD_AT_ONCE, null, null, null); case FAVOURITES: return mastodonApi.getValue().favourites(fromId, uptoId, LOAD_AT_ONCE); case BOOKMARKS: @@ -1083,53 +1188,67 @@ public class TimelineFragment extends SFragment } } - private void sendFetchTimelineRequest(@Nullable String maxId, @Nullable String sinceId, - @Nullable String sinceIdMinusOne, final FetchEnd fetchEnd, final int pos) - { - if(isAdded() && (fetchEnd == FetchEnd.TOP || fetchEnd == FetchEnd.BOTTOM && maxId == null && - progressBar.getVisibility() != View.VISIBLE) && - !isSwipeToRefreshEnabled) { + private void sendFetchTimelineRequest( + @Nullable String maxId, + @Nullable String sinceId, + @Nullable String sinceIdMinusOne, + final FetchEnd fetchEnd, + final int pos) { + if (isAdded() + && (fetchEnd == FetchEnd.TOP + || fetchEnd == FetchEnd.BOTTOM + && maxId == null + && progressBar.getVisibility() != View.VISIBLE) + && !isSwipeToRefreshEnabled) { topProgressBar.show(); } - if(kind == Kind.HOME) { + if (kind == Kind.HOME) { TimelineRequestMode mode; // allow getting old statuses/fallbacks for network only for for bottom loading - if(fetchEnd == FetchEnd.BOTTOM) { + if (fetchEnd == FetchEnd.BOTTOM) { mode = TimelineRequestMode.ANY; } else { mode = TimelineRequestMode.NETWORK; } - timelineRepo.getStatuses(maxId, sinceId, sinceIdMinusOne, LOAD_AT_ONCE, mode) - .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) - .subscribe((result) -> onFetchTimelineSuccess(result, fetchEnd, pos), - (err) -> onFetchTimelineFailure(new Exception(err), fetchEnd, pos)); + timelineRepo + .getStatuses(maxId, sinceId, sinceIdMinusOne, LOAD_AT_ONCE, mode) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + (result) -> onFetchTimelineSuccess(result, fetchEnd, pos), + (err) -> onFetchTimelineFailure(new Exception(err), fetchEnd, pos)); } else { - Callback<List<Status>> callback = new Callback<List<Status>>() { - @Override - public void onResponse(@NonNull Call<List<Status>> call, - @NonNull Response<List<Status>> response) - { - if(response.isSuccessful()) { - @Nullable String newNextId = extractNextId(response); - if(newNextId != null) { - // when we reach the bottom of the list, we won't have a new link. If - // we blindly write `null` here we will start loading from the top - // again. - nextId = newNextId; + Callback<List<Status>> callback = + new Callback<List<Status>>() { + @Override + public void onResponse( + @NonNull Call<List<Status>> call, + @NonNull Response<List<Status>> response) { + if (response.isSuccessful()) { + @Nullable String newNextId = extractNextId(response); + if (newNextId != null) { + // when we reach the bottom of the list, we won't have a new + // link. If + // we blindly write `null` here we will start loading from the + // top + // again. + nextId = newNextId; + } + onFetchTimelineSuccess( + liftStatusList(response.body()), fetchEnd, pos); + } else { + onFetchTimelineFailure( + new Exception(response.message()), fetchEnd, pos); + } } - onFetchTimelineSuccess(liftStatusList(response.body()), fetchEnd, pos); - } else { - onFetchTimelineFailure(new Exception(response.message()), fetchEnd, pos); - } - } - @Override - public void onFailure(@NonNull Call<List<Status>> call, @NonNull Throwable t) { - onFetchTimelineFailure((Exception) t, fetchEnd, pos); - } - }; + @Override + public void onFailure( + @NonNull Call<List<Status>> call, @NonNull Throwable t) { + onFetchTimelineFailure((Exception) t, fetchEnd, pos); + } + }; Call<List<Status>> listCall = getFetchCallByTimelineType(maxId, sinceId); callList.add(listCall); @@ -1140,70 +1259,72 @@ public class TimelineFragment extends SFragment @Nullable private String extractNextId(Response<?> response) { String linkHeader = response.headers().get("Link"); - if(linkHeader == null) { + if (linkHeader == null) { return null; } List<HttpHeaderLink> links = HttpHeaderLink.parse(linkHeader); HttpHeaderLink nextHeader = HttpHeaderLink.findByRelationType(links, "next"); - if(nextHeader == null) { + if (nextHeader == null) { return null; } Uri nextLink = nextHeader.uri; - if(nextLink == null) { + if (nextLink == null) { return null; } return nextLink.getQueryParameter("max_id"); } - private void onFetchTimelineSuccess(List<Either<Placeholder, Status>> statuses, - FetchEnd fetchEnd, int pos) - { + private void onFetchTimelineSuccess( + List<Either<Placeholder, Status>> statuses, FetchEnd fetchEnd, int pos) { // We filled the hole (or reached the end) if the server returned less statuses than we // we asked for. boolean fullFetch = statuses.size() >= LOAD_AT_ONCE; filterStatuses(statuses); - switch(fetchEnd) { - case TOP: { - updateStatuses(statuses, fullFetch); - break; - } - case MIDDLE: { - replacePlaceholderWithStatuses(statuses, fullFetch, pos); - break; - } - case BOTTOM: { - if(!this.statuses.isEmpty() && - !this.statuses.get(this.statuses.size() - 1).isRight()) { - this.statuses.remove(this.statuses.size() - 1); - updateAdapter(); - } - - if(!statuses.isEmpty() && !statuses.get(statuses.size() - 1).isRight()) { - // Removing placeholder if it's the last one from the cache - statuses.remove(statuses.size() - 1); - } - int oldSize = this.statuses.size(); - if(this.statuses.size() > 1) { - addItems(statuses); - } else { + switch (fetchEnd) { + case TOP: + { updateStatuses(statuses, fullFetch); + break; } - if(this.statuses.size() == oldSize) { - // This may be a brittle check but seems like it works - // Can we check it using headers somehow? Do all server support them? - didLoadEverythingBottom = true; + case MIDDLE: + { + replacePlaceholderWithStatuses(statuses, fullFetch, pos); + break; + } + case BOTTOM: + { + if (!this.statuses.isEmpty() + && !this.statuses.get(this.statuses.size() - 1).isRight()) { + this.statuses.remove(this.statuses.size() - 1); + updateAdapter(); + } + + if (!statuses.isEmpty() && !statuses.get(statuses.size() - 1).isRight()) { + // Removing placeholder if it's the last one from the cache + statuses.remove(statuses.size() - 1); + } + int oldSize = this.statuses.size(); + if (this.statuses.size() > 1) { + addItems(statuses); + } else { + updateStatuses(statuses, fullFetch); + } + if (this.statuses.size() == oldSize) { + // This may be a brittle check but seems like it works + // Can we check it using headers somehow? Do all server support them? + didLoadEverythingBottom = true; + } + break; } - break; - } } - if(isAdded()) { + if (isAdded()) { topProgressBar.hide(); updateBottomLoadingState(fetchEnd); progressBar.setVisibility(View.GONE); swipeRefreshLayout.setRefreshing(false); swipeRefreshLayout.setEnabled(true); - if(this.statuses.size() == 0) { + if (this.statuses.size() == 0) { this.showNothing(); } else { this.statusView.setVisibility(View.GONE); @@ -1212,14 +1333,14 @@ public class TimelineFragment extends SFragment } private void onFetchTimelineFailure(Exception exception, FetchEnd fetchEnd, int position) { - if(isAdded()) { + if (isAdded()) { swipeRefreshLayout.setRefreshing(false); topProgressBar.hide(); - if(fetchEnd == FetchEnd.MIDDLE && !statuses.get(position).isRight()) { + if (fetchEnd == FetchEnd.MIDDLE && !statuses.get(position).isRight()) { Placeholder placeholder = statuses.get(position).asLeftOrNull(); StatusViewData newViewData; - if(placeholder == null) { + if (placeholder == null) { Status above = statuses.get(position - 1).asRight(); String newId = StringUtils.dec(above.getId()); placeholder = new Placeholder(newId); @@ -1227,22 +1348,27 @@ public class TimelineFragment extends SFragment newViewData = new StatusViewData.Placeholder(placeholder.getId(), false); statuses.setPairedItem(position, newViewData); updateAdapter(); - } else if(this.statuses.isEmpty()) { + } else if (this.statuses.isEmpty()) { swipeRefreshLayout.setEnabled(false); this.statusView.setVisibility(View.VISIBLE); - if(exception instanceof IOException) { - this.statusView.setup(R.drawable.elephant_offline, R.string.error_network, - __ -> { - this.progressBar.setVisibility(View.VISIBLE); - this.onRefresh(); - return Unit.INSTANCE; - }); + if (exception instanceof IOException) { + this.statusView.setup( + R.drawable.elephant_offline, + R.string.error_network, + __ -> { + this.progressBar.setVisibility(View.VISIBLE); + this.onRefresh(); + return Unit.INSTANCE; + }); } else { - this.statusView.setup(R.drawable.elephant_error, R.string.error_generic, __ -> { - this.progressBar.setVisibility(View.VISIBLE); - this.onRefresh(); - return Unit.INSTANCE; - }); + this.statusView.setup( + R.drawable.elephant_error, + R.string.error_generic, + __ -> { + this.progressBar.setVisibility(View.VISIBLE); + this.onRefresh(); + return Unit.INSTANCE; + }); } } @@ -1253,44 +1379,48 @@ public class TimelineFragment extends SFragment } private void updateBottomLoadingState(FetchEnd fetchEnd) { - if(fetchEnd == FetchEnd.BOTTOM) { + if (fetchEnd == FetchEnd.BOTTOM) { bottomLoading = false; } } private void filterStatuses(List<Either<Placeholder, Status>> statuses) { Iterator<Either<Placeholder, Status>> it = statuses.iterator(); - while(it.hasNext()) { + while (it.hasNext()) { Status status = it.next().asRightOrNull(); - if(status != null && ((status.getInReplyToId() != null && filterRemoveReplies) || - (status.getReblog() != null && filterRemoveReblogs) || - shouldFilterStatus(status.getActionableStatus()))) { + if (status != null + && ((status.getInReplyToId() != null && filterRemoveReplies) + || (status.getReblog() != null && filterRemoveReblogs) + || shouldFilterStatus(status.getActionableStatus()))) { it.remove(); } } } private void updateStatuses(List<Either<Placeholder, Status>> newStatuses, boolean fullFetch) { - if(ListUtils.isEmpty(newStatuses)) { + if (ListUtils.isEmpty(newStatuses)) { updateAdapter(); return; } - if(statuses.isEmpty()) { + if (statuses.isEmpty()) { statuses.addAll(newStatuses); } else { Either<Placeholder, Status> lastOfNew = newStatuses.get(newStatuses.size() - 1); int index = statuses.indexOf(lastOfNew); - if(index >= 0) { + if (index >= 0) { statuses.subList(0, index).clear(); } int newIndex = newStatuses.indexOf(statuses.get(0)); - if(newIndex == -1) { - if(index == -1 && fullFetch) { - String placeholderId = StringUtils.inc( - CollectionsKt.last(newStatuses, Either::isRight).asRight().getId()); + if (newIndex == -1) { + if (index == -1 && fullFetch) { + String placeholderId = + StringUtils.inc( + CollectionsKt.last(newStatuses, Either::isRight) + .asRight() + .getId()); newStatuses.add(new Either.Left<>(new Placeholder(placeholderId))); } statuses.addAll(0, newStatuses); @@ -1304,27 +1434,27 @@ public class TimelineFragment extends SFragment } private void removeConsecutivePlaceholders() { - for(int i = 0; i < statuses.size() - 1; i++) { - if(statuses.get(i).isLeft() && statuses.get(i + 1).isLeft()) { + for (int i = 0; i < statuses.size() - 1; i++) { + if (statuses.get(i).isLeft() && statuses.get(i + 1).isLeft()) { statuses.remove(i); } } } private void addItems(List<Either<Placeholder, Status>> newStatuses) { - if(ListUtils.isEmpty(newStatuses)) { + if (ListUtils.isEmpty(newStatuses)) { return; } Either<Placeholder, Status> last = null; - for(int i = statuses.size() - 1; i >= 0; i--) { - if(statuses.get(i).isRight()) { + for (int i = statuses.size() - 1; i >= 0; i--) { + if (statuses.get(i).isRight()) { last = statuses.get(i); break; } } // I was about to replace findStatus with indexOf but it is incorrect to compare value // types by ID anyway and we should change equals() for Status, I think, so this makes sense - if(last != null && !newStatuses.contains(last)) { + if (last != null && !newStatuses.contains(last)) { statuses.addAll(newStatuses); removeConsecutivePlaceholders(); updateAdapter(); @@ -1338,20 +1468,19 @@ public class TimelineFragment extends SFragment CollectionsKt.removeAll(statuses, Either::isLeft); } - private void replacePlaceholderWithStatuses(List<Either<Placeholder, Status>> newStatuses, - boolean fullFetch, int pos) - { + private void replacePlaceholderWithStatuses( + List<Either<Placeholder, Status>> newStatuses, boolean fullFetch, int pos) { Either<Placeholder, Status> placeholder = statuses.get(pos); - if(placeholder.isLeft()) { + if (placeholder.isLeft()) { statuses.remove(pos); } - if(ListUtils.isEmpty(newStatuses)) { + if (ListUtils.isEmpty(newStatuses)) { updateAdapter(); return; } - if(fullFetch) { + if (fullFetch) { newStatuses.add(placeholder); } @@ -1359,16 +1488,15 @@ public class TimelineFragment extends SFragment removeConsecutivePlaceholders(); updateAdapter(); - } private int findStatusOrReblogPositionById(@NonNull String statusId) { - for(int i = 0; i < statuses.size(); i++) { + for (int i = 0; i < statuses.size(); i++) { Status status = statuses.get(i).asRightOrNull(); - if(status != null && (statusId.equals(status.getId()) || (status.getReblog() != null && - statusId.equals( - status.getReblog() - .getId())))) { + if (status != null + && (statusId.equals(status.getId()) + || (status.getReblog() != null + && statusId.equals(status.getReblog().getId())))) { return i; } } @@ -1378,19 +1506,18 @@ public class TimelineFragment extends SFragment private final Function1<Status, Either<Placeholder, Status>> statusLifter = Either.Right::new; @Nullable - private Pair<StatusViewData.Concrete, Integer> findStatusAndPosition(int position, - Status status) - { + private Pair<StatusViewData.Concrete, Integer> findStatusAndPosition( + int position, Status status) { StatusViewData.Concrete statusToUpdate; int positionToUpdate; StatusViewData someOldViewData = statuses.getPairedItem(position); // Unlikely, but data could change between the request and response - if((someOldViewData instanceof StatusViewData.Placeholder) || - !((StatusViewData.Concrete) someOldViewData).getId().equals(status.getId())) { + if ((someOldViewData instanceof StatusViewData.Placeholder) + || !((StatusViewData.Concrete) someOldViewData).getId().equals(status.getId())) { // try to find the status we need to update int foundPos = statuses.indexOf(new Either.Right<>(status)); - if(foundPos < 0) { + if (foundPos < 0) { return null; // okay, it's hopeless, give up } statusToUpdate = ((StatusViewData.Concrete) statuses.getPairedItem(foundPos)); @@ -1404,7 +1531,7 @@ public class TimelineFragment extends SFragment private void handleReblogEvent(@NonNull ReblogEvent reblogEvent) { int pos = findStatusOrReblogPositionById(reblogEvent.getStatusId()); - if(pos < 0) { + if (pos < 0) { return; } Status status = statuses.get(pos).asRight(); @@ -1413,7 +1540,7 @@ public class TimelineFragment extends SFragment private void handleFavEvent(@NonNull FavoriteEvent favEvent) { int pos = findStatusOrReblogPositionById(favEvent.getStatusId()); - if(pos < 0) { + if (pos < 0) { return; } Status status = statuses.get(pos).asRight(); @@ -1422,7 +1549,7 @@ public class TimelineFragment extends SFragment private void handleBookmarkEvent(@NonNull BookmarkEvent bookmarkEvent) { int pos = findStatusOrReblogPositionById(bookmarkEvent.getStatusId()); - if(pos < 0) { + if (pos < 0) { return; } Status status = statuses.get(pos).asRight(); @@ -1430,7 +1557,7 @@ public class TimelineFragment extends SFragment } private void handleStatusComposeEvent(@NonNull Status status) { - switch(kind) { + switch (kind) { case HOME: case PUBLIC_FEDERATED: case PUBLIC_BUBBLE: @@ -1438,7 +1565,7 @@ public class TimelineFragment extends SFragment break; case USER: case USER_WITH_REPLIES: - if(status.getAccount().getId().equals(id)) { + if (status.getAccount().getId().equals(id)) { break; } else { return; @@ -1454,15 +1581,15 @@ public class TimelineFragment extends SFragment private void handleMuteStatusEvent(MuteConversationEvent event) { int pos = findStatusOrReblogPositionById(event.getStatusId()); - if(pos < 0) { + if (pos < 0) { return; } Status eventStatus = statuses.get(pos).asRight(); String conversationId = eventStatus.getConversationId(); - if(conversationId.isEmpty()) { // invalid conversation ID - if(isFilteringMuted()) { + if (conversationId.isEmpty()) { // invalid conversation ID + if (isFilteringMuted()) { statuses.remove(pos); } else { setMutedStatusForStatus(pos, eventStatus, event.getMute(), event.getMute()); @@ -1470,12 +1597,12 @@ public class TimelineFragment extends SFragment updateAdapter(); } else { //noinspection ConstantConditions - if(isFilteringMuted()) { + if (isFilteringMuted()) { removeAllByConversationId(conversationId); } else { - for(int i = 0; i < statuses.size(); i++) { + for (int i = 0; i < statuses.size(); i++) { Status status = statuses.get(i).asRightOrNull(); - if(status != null && status.getConversationId() == conversationId) { + if (status != null && status.getConversationId() == conversationId) { setMutedStatusForStatus(i, status, event.getMute(), event.getMute()); } } @@ -1488,13 +1615,14 @@ public class TimelineFragment extends SFragment String id = event.getAccountId(); boolean muting = event.getMute(); - if(isFilteringMuted() && muting) { + if (isFilteringMuted() && muting) { removeAllByAccountId(id); } else { - for(int i = 0; i < statuses.size(); i++) { + for (int i = 0; i < statuses.size(); i++) { Status status = statuses.get(i).asRightOrNull(); - if(status != null && status.getAccount().getId().equals(id) && - !status.isThreadMuted()) { + if (status != null + && status.getAccount().getId().equals(id) + && !status.isThreadMuted()) { setMutedStatusForStatus(i, status, muting, false); } } @@ -1510,87 +1638,88 @@ public class TimelineFragment extends SFragment differ.submitList(statuses.getPairedCopy()); } - private final ListUpdateCallback listUpdateCallback = new ListUpdateCallback() { - @Override - public void onInserted(int position, int count) { - if(isAdded()) { - adapter.notifyItemRangeInserted(position, count); - Context context = getContext(); - // scroll up when new items at the top are loaded while being in the first position - // https://github.com/tuskyapp/Tusky/pull/1905#issuecomment-677819724 - if(position == 0 && context != null && adapter.getItemCount() != count) { - if(isSwipeToRefreshEnabled) { - recyclerView.scrollBy(0, Utils.dpToPx(context, -30)); - } else { - recyclerView.scrollToPosition(0); + private final ListUpdateCallback listUpdateCallback = + new ListUpdateCallback() { + @Override + public void onInserted(int position, int count) { + if (isAdded()) { + adapter.notifyItemRangeInserted(position, count); + Context context = getContext(); + // scroll up when new items at the top are loaded while being in the first + // position + // https://github.com/tuskyapp/Tusky/pull/1905#issuecomment-677819724 + if (position == 0 && context != null && adapter.getItemCount() != count) { + if (isSwipeToRefreshEnabled) { + recyclerView.scrollBy(0, Utils.dpToPx(context, -30)); + } else { + recyclerView.scrollToPosition(0); + } + } } } - } - } - - @Override - public void onRemoved(int position, int count) { - adapter.notifyItemRangeRemoved(position, count); - } - @Override - public void onMoved(int fromPosition, int toPosition) { - adapter.notifyItemMoved(fromPosition, toPosition); - } + @Override + public void onRemoved(int position, int count) { + adapter.notifyItemRangeRemoved(position, count); + } - @Override - public void onChanged(int position, int count, Object payload) { - adapter.notifyItemRangeChanged(position, count, payload); - } - }; + @Override + public void onMoved(int fromPosition, int toPosition) { + adapter.notifyItemMoved(fromPosition, toPosition); + } + @Override + public void onChanged(int position, int count, Object payload) { + adapter.notifyItemRangeChanged(position, count, payload); + } + }; - private final AsyncListDiffer<StatusViewData> differ = new AsyncListDiffer<>(listUpdateCallback, - new AsyncDifferConfig.Builder<>(diffCallback).build()); + private final AsyncListDiffer<StatusViewData> differ = + new AsyncListDiffer<>( + listUpdateCallback, new AsyncDifferConfig.Builder<>(diffCallback).build()); private final TimelineAdapter.AdapterDataSource<StatusViewData> dataSource = - new TimelineAdapter.AdapterDataSource<StatusViewData>() { - @Override - public int getItemCount() { - return differ.getCurrentList().size(); - } + new TimelineAdapter.AdapterDataSource<StatusViewData>() { + @Override + public int getItemCount() { + return differ.getCurrentList().size(); + } - @Override - public StatusViewData getItemAt(int pos) { - return differ.getCurrentList().get(pos); - } - }; + @Override + public StatusViewData getItemAt(int pos) { + return differ.getCurrentList().get(pos); + } + }; private static final DiffUtil.ItemCallback<StatusViewData> diffCallback = - new DiffUtil.ItemCallback<StatusViewData>() { + new DiffUtil.ItemCallback<StatusViewData>() { - @Override - public boolean areItemsTheSame(StatusViewData oldItem, StatusViewData newItem) { - return oldItem.getViewDataId() == newItem.getViewDataId(); - } + @Override + public boolean areItemsTheSame(StatusViewData oldItem, StatusViewData newItem) { + return oldItem.getViewDataId() == newItem.getViewDataId(); + } - @Override - public boolean areContentsTheSame(StatusViewData oldItem, - @NonNull StatusViewData newItem) - { - return false; //Items are different always. It allows to refresh timestamp on every view holder update - } + @Override + public boolean areContentsTheSame( + StatusViewData oldItem, @NonNull StatusViewData newItem) { + return false; // Items are different always. It allows to refresh timestamp on + // every view holder update + } - @Nullable - @Override - public Object getChangePayload(@NonNull StatusViewData oldItem, - @NonNull StatusViewData newItem) - { - if(oldItem.deepEquals(newItem)) { - //If items are equal - update timestamp only - return Collections.singletonList(StatusBaseViewHolder.Key.KEY_CREATED); - } else - // If items are different - update a whole view holder - { - return null; + @Nullable + @Override + public Object getChangePayload( + @NonNull StatusViewData oldItem, @NonNull StatusViewData newItem) { + if (oldItem.deepEquals(newItem)) { + // If items are equal - update timestamp only + return Collections.singletonList(StatusBaseViewHolder.Key.KEY_CREATED); + } else + // If items are different - update a whole view holder + { + return null; + } } - } - }; + }; @Override public void onResume() { @@ -1599,20 +1728,19 @@ public class TimelineFragment extends SFragment } /** - * Start to update adapter every minute to refresh timestamp - * If setting absoluteTimeView is false - * Auto dispose observable on pause + * Start to update adapter every minute to refresh timestamp If setting absoluteTimeView is + * false Auto dispose observable on pause */ private void startUpdateTimestamp() { SharedPreferences preferences = - PreferenceManager.getDefaultSharedPreferences(getActivity()); + PreferenceManager.getDefaultSharedPreferences(getActivity()); boolean useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false); - if(!useAbsoluteTime) { - Observable.interval(1, TimeUnit.MINUTES).observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_PAUSE))) - .subscribe(interval -> updateAdapter()); + if (!useAbsoluteTime) { + Observable.interval(1, TimeUnit.MINUTES) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_PAUSE))) + .subscribe(interval -> updateAdapter()); } - } @Override @@ -1622,7 +1750,7 @@ public class TimelineFragment extends SFragment @Override public void refreshContent() { - if(isAdded()) { + if (isAdded()) { onRefresh(); } else { isNeedRefresh = true; @@ -1637,7 +1765,7 @@ public class TimelineFragment extends SFragment private void setEmojiReactForStatus(int position, Status status, Status newStatus) { Pair<StatusViewData.Concrete, Integer> actual = findStatusAndPosition(position, status); - if(actual == null) { + if (actual == null) { return; } @@ -1646,7 +1774,7 @@ public class TimelineFragment extends SFragment public void handleEmojiReactEvent(EmojiReactEvent event) { int pos = findStatusOrReblogPositionById(event.getNewStatus().getActionableId()); - if(pos < 0) { + if (pos < 0) { return; } Status status = statuses.get(pos).asRight(); @@ -1656,21 +1784,29 @@ public class TimelineFragment extends SFragment @Override public void onEmojiReact(final boolean react, final String emoji, final String statusId) { int position = findStatusOrReblogPositionById(statusId); - if(position < 0) { + if (position < 0) { return; } - timelineCases.getValue().react(emoji, statusId, react) - .observeOn(AndroidSchedulers.mainThread()).as(autoDisposable(from(this))) - .subscribe((newStatus) -> setEmojiReactionForStatus(position, newStatus), - (t) -> Timber.e(t, "Failed to react with " + emoji + " on status: " + statusId)); - + timelineCases + .getValue() + .react(emoji, statusId, react) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this))) + .subscribe( + (newStatus) -> setEmojiReactionForStatus(position, newStatus), + (t) -> + Timber.e( + t, + "Failed to react with " + + emoji + + " on status: " + + statusId)); } @Override - public void onEmojiReactMenu(@NonNull View view, final EmojiReaction emoji, - final String statusId) - { + public void onEmojiReactMenu( + @NonNull View view, final EmojiReaction emoji, final String statusId) { super.emojiReactMenu(statusId, emoji, view, this); } } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java b/husky/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java index f57ac2f..13faab2 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java @@ -15,6 +15,10 @@ package com.keylesspalace.tusky.fragment; +import static com.uber.autodispose.AutoDispose.autoDisposable; +import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from; +import static org.koin.java.KoinJavaComponent.inject; + import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -65,21 +69,17 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.util.ViewDataUtils; import com.keylesspalace.tusky.view.ConversationLineItemDecoration; import com.keylesspalace.tusky.viewdata.StatusViewData; -import static com.uber.autodispose.AutoDispose.autoDisposable; -import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from; import io.reactivex.android.schedulers.AndroidSchedulers; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Locale; -import static org.koin.java.KoinJavaComponent.inject; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; public final class ViewThreadFragment extends SFragment - implements SwipeRefreshLayout.OnRefreshListener, StatusActionListener -{ + implements SwipeRefreshLayout.OnRefreshListener, StatusActionListener { private static final String TAG = "ViewThreadFragment"; @@ -95,13 +95,14 @@ public final class ViewThreadFragment extends SFragment private int statusIndex = 0; private final PairedList<Status, StatusViewData.Concrete> statuses = - new PairedList<>(new Function<Status, StatusViewData.Concrete>() { - @Override - public StatusViewData.Concrete apply(Status input) { - return ViewDataUtils.statusToViewData(input, alwaysShowSensitiveMedia, - alwaysOpenSpoiler); - } - }); + new PairedList<>( + new Function<Status, StatusViewData.Concrete>() { + @Override + public StatusViewData.Concrete apply(Status input) { + return ViewDataUtils.statusToViewData( + input, alwaysShowSensitiveMedia, alwaysOpenSpoiler); + } + }); public static ViewThreadFragment newInstance(String id) { Bundle arguments = new Bundle(1); @@ -117,25 +118,29 @@ public final class ViewThreadFragment extends SFragment thisThreadsStatusId = getArguments().getString("id"); SharedPreferences preferences = - PreferenceManager.getDefaultSharedPreferences(getActivity()); + PreferenceManager.getDefaultSharedPreferences(getActivity()); StatusDisplayOptions statusDisplayOptions = - new StatusDisplayOptions(preferences.getBoolean("animateGifAvatars", false), - accountManager.getValue().getActiveAccount().getMediaPreviewEnabled(), - preferences.getBoolean("absoluteTimeView", false), - preferences.getBoolean("showBotOverlay", true), - preferences.getBoolean("useBlurhash", true), - preferences.getBoolean("showCardsInTimelines", false) ? CardViewMode.INDENTED : - CardViewMode.NONE, preferences.getBoolean("confirmReblogs", true), - preferences.getBoolean(PrefKeys.RENDER_STATUS_AS_MENTION, true), - preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false)); + new StatusDisplayOptions( + preferences.getBoolean("animateGifAvatars", false), + accountManager.getValue().getActiveAccount().getMediaPreviewEnabled(), + preferences.getBoolean("absoluteTimeView", false), + preferences.getBoolean("showBotOverlay", true), + preferences.getBoolean("useBlurhash", true), + preferences.getBoolean("showCardsInTimelines", false) + ? CardViewMode.INDENTED + : CardViewMode.NONE, + preferences.getBoolean("confirmReblogs", true), + preferences.getBoolean(PrefKeys.RENDER_STATUS_AS_MENTION, true), + preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false)); adapter = new ThreadAdapter(statusDisplayOptions, this); } @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) - { + public View onCreateView( + @NonNull LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_view_thread, container, false); Context context = getContext(); @@ -148,14 +153,15 @@ public final class ViewThreadFragment extends SFragment LinearLayoutManager layoutManager = new LinearLayoutManager(context); recyclerView.setLayoutManager(layoutManager); recyclerView.setAccessibilityDelegateCompat( - new ListStatusAccessibilityDelegate(recyclerView, this, statuses::getPairedItemOrNull)); + new ListStatusAccessibilityDelegate( + recyclerView, this, statuses::getPairedItemOrNull)); DividerItemDecoration divider = - new DividerItemDecoration(context, layoutManager.getOrientation()); + new DividerItemDecoration(context, layoutManager.getOrientation()); recyclerView.addItemDecoration(divider); recyclerView.addItemDecoration(new ConversationLineItemDecoration(context)); alwaysShowSensitiveMedia = - accountManager.getValue().getActiveAccount().getAlwaysShowSensitiveMedia(); + accountManager.getValue().getActiveAccount().getAlwaysShowSensitiveMedia(); alwaysOpenSpoiler = accountManager.getValue().getActiveAccount().getAlwaysOpenSpoiler(); reloadFilters(PreferenceManager.getDefaultSharedPreferences(context), false); @@ -168,44 +174,47 @@ public final class ViewThreadFragment extends SFragment return rootView; } - @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); onRefresh(); - eventHub.getEvents().observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))).subscribe(event -> { - if(event instanceof FavoriteEvent) { - handleFavEvent((FavoriteEvent) event); - } else if(event instanceof ReblogEvent) { - handleReblogEvent((ReblogEvent) event); - } else if(event instanceof BookmarkEvent) { - handleBookmarkEvent((BookmarkEvent) event); - } else if(event instanceof BlockEvent) { - removeAllByAccountId(((BlockEvent) event).getAccountId()); - } else if(event instanceof MuteEvent) { - handleMuteEvent((MuteEvent) event); - } else if(event instanceof StatusComposedEvent) { - handleStatusComposedEvent((StatusComposedEvent) event); - } else if(event instanceof StatusDeletedEvent) { - handleStatusDeletedEvent((StatusDeletedEvent) event); - } else if(event instanceof EmojiReactEvent) { - handleEmojiReactEvent((EmojiReactEvent) event); - } - }); - - if(thisThreadsStatusPosition != -1) { + eventHub.getEvents() + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + event -> { + if (event instanceof FavoriteEvent) { + handleFavEvent((FavoriteEvent) event); + } else if (event instanceof ReblogEvent) { + handleReblogEvent((ReblogEvent) event); + } else if (event instanceof BookmarkEvent) { + handleBookmarkEvent((BookmarkEvent) event); + } else if (event instanceof BlockEvent) { + removeAllByAccountId(((BlockEvent) event).getAccountId()); + } else if (event instanceof MuteEvent) { + handleMuteEvent((MuteEvent) event); + } else if (event instanceof StatusComposedEvent) { + handleStatusComposedEvent((StatusComposedEvent) event); + } else if (event instanceof StatusDeletedEvent) { + handleStatusDeletedEvent((StatusDeletedEvent) event); + } else if (event instanceof EmojiReactEvent) { + handleEmojiReactEvent((EmojiReactEvent) event); + } + }); + + if (thisThreadsStatusPosition != -1) { recyclerView.scrollToPosition(thisThreadsStatusPosition); } } public void onRevealPressed() { boolean allExpanded = allExpanded(); - for(int i = 0; i < statuses.size(); i++) { + for (int i = 0; i < statuses.size(); i++) { StatusViewData.Concrete newViewData = - new StatusViewData.Concrete.Builder(statuses.getPairedItem(i)).setIsExpanded( - !allExpanded).createStatusViewData(); + new StatusViewData.Concrete.Builder(statuses.getPairedItem(i)) + .setIsExpanded(!allExpanded) + .createStatusViewData(); statuses.setPairedItem(i, newViewData); } updateAdapter(); @@ -214,8 +223,8 @@ public final class ViewThreadFragment extends SFragment private boolean allExpanded() { boolean allExpanded = true; - for(int i = 0; i < statuses.size(); i++) { - if(!statuses.getPairedItem(i).isExpanded()) { + for (int i = 0; i < statuses.size(); i++) { + if (!statuses.getPairedItem(i).isExpanded()) { allExpanded = false; break; } @@ -238,48 +247,59 @@ public final class ViewThreadFragment extends SFragment public void onReblog(final boolean reblog, final int position) { final Status status = statuses.get(position); - timelineCases.getValue().reblog(statuses.get(position), reblog) - .observeOn(AndroidSchedulers.mainThread()).as(autoDisposable(from(this))) - .subscribe((newStatus) -> updateStatus(position, newStatus), - (t) -> Log.d(TAG, "Failed to reblog status: " + status.getId(), t)); + timelineCases + .getValue() + .reblog(statuses.get(position), reblog) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this))) + .subscribe( + (newStatus) -> updateStatus(position, newStatus), + (t) -> Log.d(TAG, "Failed to reblog status: " + status.getId(), t)); } @Override public void onFavourite(final boolean favourite, final int position) { final Status status = statuses.get(position); - timelineCases.getValue().favourite(statuses.get(position), favourite) - .observeOn(AndroidSchedulers.mainThread()).as(autoDisposable(from(this))) - .subscribe((newStatus) -> updateStatus(position, newStatus), - (t) -> Log.d(TAG, "Failed to favourite status: " + status.getId(), t)); + timelineCases + .getValue() + .favourite(statuses.get(position), favourite) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this))) + .subscribe( + (newStatus) -> updateStatus(position, newStatus), + (t) -> Log.d(TAG, "Failed to favourite status: " + status.getId(), t)); } @Override public void onBookmark(final boolean bookmark, final int position) { final Status status = statuses.get(position); - timelineCases.getValue().bookmark(statuses.get(position), bookmark) - .observeOn(AndroidSchedulers.mainThread()).as(autoDisposable(from(this))) - .subscribe((newStatus) -> updateStatus(position, newStatus), - (t) -> Log.d(TAG, "Failed to bookmark status: " + status.getId(), t)); + timelineCases + .getValue() + .bookmark(statuses.get(position), bookmark) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this))) + .subscribe( + (newStatus) -> updateStatus(position, newStatus), + (t) -> Log.d(TAG, "Failed to bookmark status: " + status.getId(), t)); } private void updateStatus(int position, Status status) { - if(position >= 0 && position < statuses.size()) { + if (position >= 0 && position < statuses.size()) { Status actionableStatus = status.getActionableStatus(); StatusViewData.Concrete viewData = - new StatusViewData.Builder(statuses.getPairedItem(position)).setReblogged( - actionableStatus.getReblogged()) - .setReblogsCount(actionableStatus.getReblogsCount()) - .setFavourited(actionableStatus.getFavourited()) - .setBookmarked(actionableStatus.getBookmarked()) - .setFavouritesCount(actionableStatus.getFavouritesCount()) - .createStatusViewData(); + new StatusViewData.Builder(statuses.getPairedItem(position)) + .setReblogged(actionableStatus.getReblogged()) + .setReblogsCount(actionableStatus.getReblogsCount()) + .setFavourited(actionableStatus.getFavourited()) + .setBookmarked(actionableStatus.getBookmarked()) + .setFavouritesCount(actionableStatus.getFavouritesCount()) + .createStatusViewData(); statuses.setPairedItem(position, viewData); adapter.setItem(position, viewData, true); - } } @@ -297,7 +317,7 @@ public final class ViewThreadFragment extends SFragment @Override public void onViewThread(int position) { Status status = statuses.get(position); - if(thisThreadsStatusId.equals(status.getId())) { + if (thisThreadsStatusId.equals(status.getId())) { // If already viewing this thread, don't reopen it. return; } @@ -307,7 +327,7 @@ public final class ViewThreadFragment extends SFragment @Override public void onViewReplyTo(int position) { Status status = statuses.get(position); - if(thisThreadsStatusId.equals(status.getInReplyToId())) { + if (thisThreadsStatusId.equals(status.getInReplyToId())) { return; } super.onShowReplyTo(status.getInReplyToId()); @@ -322,8 +342,9 @@ public final class ViewThreadFragment extends SFragment @Override public void onExpandedChange(boolean expanded, int position) { StatusViewData.Concrete newViewData = - new StatusViewData.Builder(statuses.getPairedItem(position)).setIsExpanded(expanded) - .createStatusViewData(); + new StatusViewData.Builder(statuses.getPairedItem(position)) + .setIsExpanded(expanded) + .createStatusViewData(); statuses.setPairedItem(position, newViewData); adapter.setItem(position, newViewData, true); updateRevealIcon(); @@ -331,24 +352,23 @@ public final class ViewThreadFragment extends SFragment @Override public void onContentHiddenChange(boolean isShowing, int position) { - StatusViewData.Concrete newViewData = new StatusViewData.Builder( - statuses.getPairedItem(position)).setIsShowingSensitiveContent(isShowing) - .createStatusViewData(); + StatusViewData.Concrete newViewData = + new StatusViewData.Builder(statuses.getPairedItem(position)) + .setIsShowingSensitiveContent(isShowing) + .createStatusViewData(); statuses.setPairedItem(position, newViewData); adapter.setItem(position, newViewData, true); } @Override - public void onLoadMore(int position) { - - } + public void onLoadMore(int position) {} @Override public void onShowReblogs(int position) { String statusId = statuses.get(position).getId(); Intent intent = - AccountListActivity.newIntent(getContext(), AccountListActivity.Type.REBLOGGED, - statusId); + AccountListActivity.newIntent( + getContext(), AccountListActivity.Type.REBLOGGED, statusId); ((BaseActivity) getActivity()).startActivityWithSlideInAnimation(intent); } @@ -356,32 +376,38 @@ public final class ViewThreadFragment extends SFragment public void onShowFavs(int position) { String statusId = statuses.get(position).getId(); Intent intent = - AccountListActivity.newIntent(getContext(), AccountListActivity.Type.FAVOURITED, - statusId); + AccountListActivity.newIntent( + getContext(), AccountListActivity.Type.FAVOURITED, statusId); ((BaseActivity) getActivity()).startActivityWithSlideInAnimation(intent); } @Override public void onContentCollapsedChange(boolean isCollapsed, int position) { - if(position < 0 || position >= statuses.size()) { - Log.e(TAG, - String.format("Tried to access out of bounds status position: %d of %d", position, - statuses.size() - 1)); + if (position < 0 || position >= statuses.size()) { + Log.e( + TAG, + String.format( + "Tried to access out of bounds status position: %d of %d", + position, statuses.size() - 1)); return; } StatusViewData.Concrete status = statuses.getPairedItem(position); - if(status == null) { + if (status == null) { // Statuses PairedList contains a base type of StatusViewData.Concrete and also doesn't - // check for null values when adding values to it although this doesn't seem to be an issue. - Log.e(TAG, String.format( - "Expected StatusViewData.Concrete, got null instead at position: %d of %d", - position, statuses.size() - 1)); + // check for null values when adding values to it although this doesn't seem to be an + // issue. + Log.e( + TAG, + String.format( + "Expected StatusViewData.Concrete, got null instead at position: %d of" + + " %d", + position, statuses.size() - 1)); return; } StatusViewData.Concrete updatedStatus = - new StatusViewData.Builder(status).setCollapsed(isCollapsed).createStatusViewData(); + new StatusViewData.Builder(status).setCollapsed(isCollapsed).createStatusViewData(); statuses.setPairedItem(position, updatedStatus); recyclerView.post(() -> adapter.setItem(position, updatedStatus, true)); } @@ -398,8 +424,8 @@ public final class ViewThreadFragment extends SFragment @Override public void removeItem(int position) { - if(position == statusIndex) { - //the status got removed, close the activity + if (position == statusIndex) { + // the status got removed, close the activity getActivity().finish(); } statuses.remove(position); @@ -411,11 +437,14 @@ public final class ViewThreadFragment extends SFragment setVoteForPoll(position, status.getPoll().votedCopy(choices)); - timelineCases.getValue().voteInPoll(status, choices) - .observeOn(AndroidSchedulers.mainThread()).as(autoDisposable(from(this))) - .subscribe((newPoll) -> setVoteForPoll(position, newPoll), - (t) -> Log.d(TAG, "Failed to vote in poll: " + status.getId(), t)); - + timelineCases + .getValue() + .voteInPoll(status, choices) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this))) + .subscribe( + (newPoll) -> setVoteForPoll(position, newPoll), + (t) -> Log.d(TAG, "Failed to vote in poll: " + status.getId(), t)); } private void setVoteForPoll(int position, Poll newPoll) { @@ -423,7 +452,7 @@ public final class ViewThreadFragment extends SFragment StatusViewData.Concrete viewData = statuses.getPairedItem(position); StatusViewData.Concrete newViewData = - new StatusViewData.Builder(viewData).setPoll(newPoll).createStatusViewData(); + new StatusViewData.Builder(viewData).setPoll(newPoll).createStatusViewData(); statuses.setPairedItem(position, newViewData); adapter.setItem(position, newViewData, true); } @@ -434,21 +463,21 @@ public final class ViewThreadFragment extends SFragment private void removeAllByAccountId(String accountId) { Status status = null; - if(!statuses.isEmpty()) { + if (!statuses.isEmpty()) { status = statuses.get(statusIndex); } // using iterator to safely remove items while iterating Iterator<Status> iterator = statuses.iterator(); - while(iterator.hasNext()) { + while (iterator.hasNext()) { Status s = iterator.next(); - if(s.getAccount().getId().equals(accountId) || - s.getActionableStatus().getAccount().getId().equals(accountId)) { + if (s.getAccount().getId().equals(accountId) + || s.getActionableStatus().getAccount().getId().equals(accountId)) { iterator.remove(); } } statusIndex = statuses.indexOf(status); - if(statusIndex == -1) { - //the status got removed, close the activity + if (statusIndex == -1) { + // the status got removed, close the activity getActivity().finish(); return; } @@ -458,66 +487,73 @@ public final class ViewThreadFragment extends SFragment private void sendStatusRequest(final String id) { Call<Status> call = mastodonApi.getValue().status(id); - call.enqueue(new Callback<Status>() { - @Override - public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) { - if(response.isSuccessful()) { - int position = setStatus(response.body()); - recyclerView.scrollToPosition(position); - } else { - onThreadRequestFailure(id); - } - } - - @Override - public void onFailure(@NonNull Call<Status> call, @NonNull Throwable t) { - onThreadRequestFailure(id); - } - }); + call.enqueue( + new Callback<Status>() { + @Override + public void onResponse( + @NonNull Call<Status> call, @NonNull Response<Status> response) { + if (response.isSuccessful()) { + int position = setStatus(response.body()); + recyclerView.scrollToPosition(position); + } else { + onThreadRequestFailure(id); + } + } + + @Override + public void onFailure(@NonNull Call<Status> call, @NonNull Throwable t) { + onThreadRequestFailure(id); + } + }); callList.add(call); } private void sendThreadRequest(final String id) { Call<StatusContext> call = mastodonApi.getValue().statusContext(id); - call.enqueue(new Callback<StatusContext>() { - @Override - public void onResponse(@NonNull Call<StatusContext> call, - @NonNull Response<StatusContext> response) - { - StatusContext context = response.body(); - if(response.isSuccessful() && context != null) { - swipeRefreshLayout.setRefreshing(false); - setContext(context.getAncestors(), context.getDescendants()); - } else { - onThreadRequestFailure(id); - } - } - - @Override - public void onFailure(@NonNull Call<StatusContext> call, @NonNull Throwable t) { - onThreadRequestFailure(id); - } - }); + call.enqueue( + new Callback<StatusContext>() { + @Override + public void onResponse( + @NonNull Call<StatusContext> call, + @NonNull Response<StatusContext> response) { + StatusContext context = response.body(); + if (response.isSuccessful() && context != null) { + swipeRefreshLayout.setRefreshing(false); + setContext(context.getAncestors(), context.getDescendants()); + } else { + onThreadRequestFailure(id); + } + } + + @Override + public void onFailure(@NonNull Call<StatusContext> call, @NonNull Throwable t) { + onThreadRequestFailure(id); + } + }); callList.add(call); } private void onThreadRequestFailure(final String id) { View view = getView(); swipeRefreshLayout.setRefreshing(false); - if(view != null) { + if (view != null) { Snackbar.make(view, R.string.error_generic, Snackbar.LENGTH_LONG) - .setAction(R.string.action_retry, v -> { - sendThreadRequest(id); - sendStatusRequest(id); - }).show(); + .setAction( + R.string.action_retry, + v -> { + sendThreadRequest(id); + sendStatusRequest(id); + }) + .show(); } else { Log.e(TAG, "Couldn't display thread fetch error message"); } } private int setStatus(Status status) { - if(statuses.size() > 0 && statusIndex < statuses.size() && - statuses.get(statusIndex).equals(status)) { + if (statuses.size() > 0 + && statusIndex < statuses.size() + && statuses.get(statusIndex).equals(status)) { // Do not add this status on refresh, it's already in there. statuses.set(statusIndex, status); return statusIndex; @@ -536,15 +572,15 @@ public final class ViewThreadFragment extends SFragment // In case of refresh, remove old ancestors and descendants first. We'll remove all blindly, // as we have no guarantee on their order to be the same as before int oldSize = statuses.size(); - if(oldSize > 1) { + if (oldSize > 1) { mainStatus = statuses.get(statusIndex); statuses.clear(); adapter.clearItems(); } ArrayList<Status> ancestors = new ArrayList<>(); - for(Status status : unfilteredAncestors) { - if(!shouldFilterStatus(status)) { + for (Status status : unfilteredAncestors) { + if (!shouldFilterStatus(status)) { ancestors.add(status); } } @@ -554,17 +590,22 @@ public final class ViewThreadFragment extends SFragment adapter.setDetailedStatusPosition(statusIndex); statuses.addAll(0, ancestors); List<StatusViewData.Concrete> ancestorsViewDatas = - statuses.getPairedCopy().subList(0, statusIndex); - if(BuildConfig.DEBUG && ancestors.size() != ancestorsViewDatas.size()) { - String error = String.format(Locale.getDefault(), - "Incorrectly got statusViewData sublist." + - " ancestors.size == %d ancestorsViewDatas.size == %d," + " statuses.size == %d", - ancestors.size(), ancestorsViewDatas.size(), statuses.size()); + statuses.getPairedCopy().subList(0, statusIndex); + if (BuildConfig.DEBUG && ancestors.size() != ancestorsViewDatas.size()) { + String error = + String.format( + Locale.getDefault(), + "Incorrectly got statusViewData sublist." + + " ancestors.size == %d ancestorsViewDatas.size == %d," + + " statuses.size == %d", + ancestors.size(), + ancestorsViewDatas.size(), + statuses.size()); throw new AssertionError(error); } adapter.addAll(0, ancestorsViewDatas); - if(mainStatus != null) { + if (mainStatus != null) { // In case we needed to delete everything (which is way easier than deleting // everything except one), re-insert the remaining status here. // Not filtering the main status, since the user explicitly chose to be here @@ -575,8 +616,8 @@ public final class ViewThreadFragment extends SFragment } ArrayList<Status> descendants = new ArrayList<>(); - for(Status status : unfilteredDescendants) { - if(!shouldFilterStatus(status)) { + for (Status status : unfilteredDescendants) { + if (!shouldFilterStatus(status)) { descendants.add(status); } } @@ -585,12 +626,18 @@ public final class ViewThreadFragment extends SFragment statuses.addAll(descendants); List<StatusViewData.Concrete> descendantsViewData; descendantsViewData = - statuses.getPairedCopy().subList(statuses.size() - descendants.size(), statuses.size()); - if(BuildConfig.DEBUG && descendants.size() != descendantsViewData.size()) { - String error = String.format(Locale.getDefault(), - "Incorrectly got statusViewData sublist." + - " descendants.size == %d descendantsViewData.size == %d," + " statuses.size == %d", - descendants.size(), descendantsViewData.size(), statuses.size()); + statuses.getPairedCopy() + .subList(statuses.size() - descendants.size(), statuses.size()); + if (BuildConfig.DEBUG && descendants.size() != descendantsViewData.size()) { + String error = + String.format( + Locale.getDefault(), + "Incorrectly got statusViewData sublist." + + " descendants.size == %d descendantsViewData.size == %d," + + " statuses.size == %d", + descendants.size(), + descendantsViewData.size(), + statuses.size()); throw new AssertionError(error); } adapter.addAll(descendantsViewData); @@ -599,7 +646,7 @@ public final class ViewThreadFragment extends SFragment private void setMutedStatusForStatus(int position, Status status, boolean muted) { StatusViewData.Builder statusViewData = - new StatusViewData.Builder(statuses.getPairedItem(position)); + new StatusViewData.Builder(statuses.getPairedItem(position)); statusViewData.setMuted(muted); statuses.setPairedItem(position, statusViewData.createStatusViewData()); @@ -609,13 +656,14 @@ public final class ViewThreadFragment extends SFragment String id = event.getAccountId(); boolean muting = event.getMute(); - if(isFilteringMuted()) { + if (isFilteringMuted()) { removeAllByAccountId(id); } else { - for(int i = 0; i < statuses.size(); i++) { + for (int i = 0; i < statuses.size(); i++) { Status status = statuses.get(i); - if(status != null && status.getAccount().getId().equals(id) && - !status.isThreadMuted()) { + if (status != null + && status.getAccount().getId().equals(id) + && !status.isThreadMuted()) { setMutedStatusForStatus(i, status, muting); } } @@ -623,17 +671,16 @@ public final class ViewThreadFragment extends SFragment } } - private void handleFavEvent(FavoriteEvent event) { Pair<Integer, Status> posAndStatus = findStatusAndPos(event.getStatusId()); - if(posAndStatus == null) { + if (posAndStatus == null) { return; } boolean favourite = event.getFavourite(); posAndStatus.second.setFavourited(favourite); - if(posAndStatus.second.getReblog() != null) { + if (posAndStatus.second.getReblog() != null) { posAndStatus.second.getReblog().setFavourited(favourite); } @@ -650,14 +697,14 @@ public final class ViewThreadFragment extends SFragment private void handleReblogEvent(ReblogEvent event) { Pair<Integer, Status> posAndStatus = findStatusAndPos(event.getStatusId()); - if(posAndStatus == null) { + if (posAndStatus == null) { return; } boolean reblog = event.getReblog(); posAndStatus.second.setReblogged(reblog); - if(posAndStatus.second.getReblog() != null) { + if (posAndStatus.second.getReblog() != null) { posAndStatus.second.getReblog().setReblogged(reblog); } @@ -674,14 +721,14 @@ public final class ViewThreadFragment extends SFragment private void handleBookmarkEvent(BookmarkEvent event) { Pair<Integer, Status> posAndStatus = findStatusAndPos(event.getStatusId()); - if(posAndStatus == null) { + if (posAndStatus == null) { return; } boolean bookmark = event.getBookmark(); posAndStatus.second.setBookmarked(bookmark); - if(posAndStatus.second.getReblog() != null) { + if (posAndStatus.second.getReblog() != null) { posAndStatus.second.getReblog().setBookmarked(bookmark); } @@ -698,18 +745,18 @@ public final class ViewThreadFragment extends SFragment private void handleStatusComposedEvent(StatusComposedEvent event) { Status eventStatus = event.getStatus(); - if(eventStatus.getInReplyToId() == null) { + if (eventStatus.getInReplyToId() == null) { return; } - if(eventStatus.getInReplyToId().equals(thisThreadsStatusId)) { + if (eventStatus.getInReplyToId().equals(thisThreadsStatusId)) { insertStatus(eventStatus, statuses.size()); } else { // If new status is a reply to some status in the thread, insert new status after it // We only check statuses below main status, ones on top don't belong to this thread - for(int i = statusIndex; i < statuses.size(); i++) { + for (int i = statusIndex; i < statuses.size(); i++) { Status status = statuses.get(i); - if(eventStatus.getInReplyToId().equals(status.getId())) { + if (eventStatus.getInReplyToId().equals(status.getId())) { insertStatus(eventStatus, i + 1); break; } @@ -722,26 +769,27 @@ public final class ViewThreadFragment extends SFragment private void insertStatus(Status status, int at) { statuses.add(at, status); adapter.addItem(at, statuses.getPairedItem(at)); - if(status.getId().equals(thisThreadsStatusId)) { + if (status.getId().equals(thisThreadsStatusId)) { thisThreadsStatusPosition = at; } } private void handleStatusDeletedEvent(StatusDeletedEvent event) { Pair<Integer, Status> posAndStatus = findStatusAndPos(event.getStatusId()); - if(posAndStatus == null) { + if (posAndStatus == null) { return; } - @SuppressWarnings("ConstantConditions") int pos = posAndStatus.first; + @SuppressWarnings("ConstantConditions") + int pos = posAndStatus.first; statuses.remove(pos); adapter.removeItem(pos); } @Nullable private Pair<Integer, Status> findStatusAndPos(@NonNull String statusId) { - for(int i = 0; i < statuses.size(); i++) { - if(statusId.equals(statuses.get(i).getId())) { + for (int i = 0; i < statuses.size(); i++) { + if (statusId.equals(statuses.get(i).getId())) { return new Pair<>(i, statuses.get(i)); } } @@ -750,24 +798,26 @@ public final class ViewThreadFragment extends SFragment private void updateRevealIcon() { ViewThreadActivity activity = ((ViewThreadActivity) getActivity()); - if(activity == null) { + if (activity == null) { return; } boolean hasAnyWarnings = false; // Statuses are updated from the main thread so nothing should change while iterating - for(int i = 0; i < statuses.size(); i++) { - if(!TextUtils.isEmpty(statuses.get(i).getSpoilerText())) { + for (int i = 0; i < statuses.size(); i++) { + if (!TextUtils.isEmpty(statuses.get(i).getSpoilerText())) { hasAnyWarnings = true; break; } } - if(!hasAnyWarnings) { + if (!hasAnyWarnings) { activity.setRevealButtonState(ViewThreadActivity.REVEAL_BUTTON_HIDDEN); return; } - activity.setRevealButtonState(allExpanded() ? ViewThreadActivity.REVEAL_BUTTON_HIDE : - ViewThreadActivity.REVEAL_BUTTON_REVEAL); + activity.setRevealButtonState( + allExpanded() + ? ViewThreadActivity.REVEAL_BUTTON_HIDE + : ViewThreadActivity.REVEAL_BUTTON_REVEAL); } @Override @@ -789,8 +839,8 @@ public final class ViewThreadFragment extends SFragment public void handleEmojiReactEvent(EmojiReactEvent event) { Pair<Integer, Status> posAndStatus = - findStatusAndPos(event.getNewStatus().getActionableId()); - if(posAndStatus == null) { + findStatusAndPos(event.getNewStatus().getActionableId()); + if (posAndStatus == null) { return; } setEmojiReactionForStatus(posAndStatus.first, event.getNewStatus()); @@ -800,21 +850,28 @@ public final class ViewThreadFragment extends SFragment public void onEmojiReact(final boolean react, final String emoji, final String statusId) { Pair<Integer, Status> statusAndPos = findStatusAndPos(statusId); - if(statusAndPos == null) { + if (statusAndPos == null) { return; } int position = statusAndPos.first; - timelineCases.getValue().react(emoji, statusId, react) - .observeOn(AndroidSchedulers.mainThread()).as(autoDisposable(from(this))) - .subscribe((newStatus) -> setEmojiReactionForStatus(position, newStatus), - (t) -> Log.d(TAG, "Failed to react with " + emoji + " on status: " + statusId, t)); + timelineCases + .getValue() + .react(emoji, statusId, react) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this))) + .subscribe( + (newStatus) -> setEmojiReactionForStatus(position, newStatus), + (t) -> + Log.d( + TAG, + "Failed to react with " + emoji + " on status: " + statusId, + t)); } @Override - public void onEmojiReactMenu(@NonNull View view, final EmojiReaction emoji, - final String statusId) - { + public void onEmojiReactMenu( + @NonNull View view, final EmojiReaction emoji, final String statusId) { super.emojiReactMenu(statusId, emoji, view, this); } } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/interfaces/AccountActionListener.java b/husky/app/src/main/java/com/keylesspalace/tusky/interfaces/AccountActionListener.java index c353d0f..3bdedfc 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/interfaces/AccountActionListener.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/interfaces/AccountActionListener.java @@ -17,7 +17,11 @@ package com.keylesspalace.tusky.interfaces; public interface AccountActionListener { void onViewAccount(String id); - void onMute(final boolean mute, final String id, final int position, final boolean notifications); + + void onMute( + final boolean mute, final String id, final int position, final boolean notifications); + void onBlock(final boolean block, final String id, final int position); + void onRespondToFollowRequest(final boolean accept, final String id, final int position); } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/interfaces/LinkListener.java b/husky/app/src/main/java/com/keylesspalace/tusky/interfaces/LinkListener.java index 90599b2..369703c 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/interfaces/LinkListener.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/interfaces/LinkListener.java @@ -17,6 +17,8 @@ package com.keylesspalace.tusky.interfaces; public interface LinkListener { void onViewTag(String tag); + void onViewAccount(String id); + void onViewUrl(String url); } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/interfaces/PermissionRequester.java b/husky/app/src/main/java/com/keylesspalace/tusky/interfaces/PermissionRequester.java index ca83e08..a87e87b 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/interfaces/PermissionRequester.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/interfaces/PermissionRequester.java @@ -2,4 +2,4 @@ package com.keylesspalace.tusky.interfaces; public interface PermissionRequester { void onRequestPermissionsResult(String[] permissions, int[] grantResults); -}
\ No newline at end of file +} diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java b/husky/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java index d956cec..22868f8 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java @@ -16,30 +16,39 @@ package com.keylesspalace.tusky.interfaces; import android.view.View; - -import java.util.List; -import com.keylesspalace.tusky.entity.EmojiReaction; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.keylesspalace.tusky.entity.EmojiReaction; +import java.util.List; public interface StatusActionListener extends LinkListener { void onReply(int position); + void onReblog(final boolean reblog, final int position); + void onFavourite(final boolean favourite, final int position); + void onBookmark(final boolean bookmark, final int position); + void onMore(@NonNull View view, final int position); + void onViewMedia(int position, int attachmentIndex, @Nullable View view); + void onViewThread(int position); + void onViewReplyTo(int position); /** * Open reblog author for the status. + * * @param position At which position in the list status is located */ void onOpenReblog(int position); + void onExpandedChange(boolean expanded, int position); + void onContentHiddenChange(boolean isShowing, int position); + void onLoadMore(int position); /** @@ -47,26 +56,37 @@ public interface StatusActionListener extends LinkListener { * status content is interacted with. * * @param isCollapsed Whether the status content is shown in a collapsed state or fully. - * @param position The position of the status in the list. + * @param position The position of the status in the list. */ void onContentCollapsedChange(boolean isCollapsed, int position); /** * called when the reblog count has been clicked + * * @param position The position of the status in the list. */ default void onShowReblogs(int position) {} /** * called when the favourite count has been clicked + * * @param position The position of the status in the list. */ default void onShowFavs(int position) {} void onVoteInPoll(int position, @NonNull List<Integer> choices); - + default void onMute(int position, boolean isMuted) {} - default void onEmojiReact(@NonNull final boolean react, @NonNull final String emoji, @NonNull final String statusId) {}; - default void onEmojiReactMenu(@NonNull View view, @NonNull final EmojiReaction emoji, @NonNull final String statusId) {}; + default void onEmojiReact( + @NonNull final boolean react, + @NonNull final String emoji, + @NonNull final String statusId) {} + ; + + default void onEmojiReactMenu( + @NonNull View view, + @NonNull final EmojiReaction emoji, + @NonNull final String statusId) {} + ; } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.java b/husky/app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.java index cd44f6d..7427923 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.java @@ -43,21 +43,22 @@ public final class InstanceSwitchAuthInterceptor implements Interceptor { Request originalRequest = chain.request(); // Only switch domains if the request comes from retrofit - if(originalRequest.url().host().equals(MastodonApi.PLACEHOLDER_DOMAIN)) { + if (originalRequest.url().host().equals(MastodonApi.PLACEHOLDER_DOMAIN)) { AccountEntity currentAccount = accountManager.getActiveAccount(); Request.Builder builder = originalRequest.newBuilder(); String instanceHeader = originalRequest.header(MastodonApi.DOMAIN_HEADER); - if(instanceHeader != null) { + if (instanceHeader != null) { // use domain explicitly specified in custom header builder.url(swapHost(originalRequest.url(), instanceHeader)); builder.removeHeader(MastodonApi.DOMAIN_HEADER); - } else if(currentAccount != null) { - //use domain of current account + } else if (currentAccount != null) { + // use domain of current account builder.url(swapHost(originalRequest.url(), currentAccount.getDomain())) - .header("Authorization", - String.format("Bearer %s", currentAccount.getAccessToken())); + .header( + "Authorization", + String.format("Bearer %s", currentAccount.getAccessToken())); } Request newRequest = builder.build(); diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/network/ProgressRequestBody.java b/husky/app/src/main/java/com/keylesspalace/tusky/network/ProgressRequestBody.java index d559d35..b1faeba 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/network/ProgressRequestBody.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/network/ProgressRequestBody.java @@ -16,10 +16,8 @@ package com.keylesspalace.tusky.network; import androidx.annotation.NonNull; - import java.io.IOException; import java.io.InputStream; - import okhttp3.MediaType; import okhttp3.RequestBody; import okio.BufferedSink; @@ -36,7 +34,11 @@ public final class ProgressRequestBody extends RequestBody { void onProgressUpdate(int percentage); } - public ProgressRequestBody(final InputStream content, long contentLength, final MediaType mediaType, final UploadCallback listener) { + public ProgressRequestBody( + final InputStream content, + long contentLength, + final MediaType mediaType, + final UploadCallback listener) { this.content = content; this.contentLength = contentLength; this.mediaType = mediaType; @@ -62,7 +64,7 @@ public final class ProgressRequestBody extends RequestBody { try { int read; while ((read = content.read(buffer)) != -1) { - uploadListener.onProgressUpdate((int)(100 * uploaded / contentLength)); + uploadListener.onProgressUpdate((int) (100 * uploaded / contentLength)); uploaded += read; sink.write(buffer, 0, read); @@ -71,4 +73,4 @@ public final class ProgressRequestBody extends RequestBody { content.close(); } } -}
\ No newline at end of file +} diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/util/BBCodeEdit.java b/husky/app/src/main/java/com/keylesspalace/tusky/util/BBCodeEdit.java index f67e3ad..0e4af19 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/util/BBCodeEdit.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/util/BBCodeEdit.java @@ -1,17 +1,15 @@ package com.keylesspalace.tusky.util; -import androidx.annotation.IntDef; -import androidx.annotation.IntRange; -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; import android.text.Editable; import android.text.Selection; -import android.text.Spannable; import android.widget.EditText; +import androidx.annotation.NonNull; import me.thanel.markdownedit.SelectionUtils; public class BBCodeEdit { - private BBCodeEdit() { /* cannot be instantiated */ } + private BBCodeEdit() { + /* cannot be instantiated */ + } public static void addBold(@NonNull Editable text) { HTMLEdit.surroundSelectionWith(text, "[b]", "[/b]"); @@ -61,7 +59,7 @@ public class BBCodeEdit { public static void addLink(@NonNull EditText editText) { addLink(editText.getText()); } - + /** * Inserts a markdown code block to the specified EditText at the currently selected position. * @@ -80,4 +78,3 @@ public class BBCodeEdit { addCode(editText.getText()); } } - diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/util/CustomURLSpan.java b/husky/app/src/main/java/com/keylesspalace/tusky/util/CustomURLSpan.java index e772162..f563f9f 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/util/CustomURLSpan.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/util/CustomURLSpan.java @@ -15,26 +15,27 @@ public class CustomURLSpan extends URLSpan { super(src); } - public static final Parcelable.Creator<CustomURLSpan> CREATOR = new Parcelable.Creator<CustomURLSpan>() { + public static final Parcelable.Creator<CustomURLSpan> CREATOR = + new Parcelable.Creator<CustomURLSpan>() { - @Override - public CustomURLSpan createFromParcel(Parcel source) { - return new CustomURLSpan(source); - } + @Override + public CustomURLSpan createFromParcel(Parcel source) { + return new CustomURLSpan(source); + } - @Override - public CustomURLSpan[] newArray(int size) { - return new CustomURLSpan[size]; - } - - }; + @Override + public CustomURLSpan[] newArray(int size) { + return new CustomURLSpan[size]; + } + }; @Override public void onClick(View view) { LinkHelper.openLink(getURL(), view.getContext()); } - @Override public void updateDrawState(TextPaint ds) { + @Override + public void updateDrawState(TextPaint ds) { super.updateDrawState(ds); ds.setUnderlineText(false); } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/util/Emojis.java b/husky/app/src/main/java/com/keylesspalace/tusky/util/Emojis.java index 61de258..3271e79 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/util/Emojis.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/util/Emojis.java @@ -1,1084 +1,1084 @@ package com.keylesspalace.tusky.util; // AUTOGENERATED -public class Emojis -{ - public static final String[][] EMOJIS = new String[][] { -{ // # group: Smileys & Emotion -"😀", // 1F600 ; fully-qualified # 😀 grinning face -"😃", // 1F603 ; fully-qualified # 😃 grinning face with big eyes -"😄", // 1F604 ; fully-qualified # 😄 grinning face with smiling eyes -"😁", // 1F601 ; fully-qualified # 😁 beaming face with smiling eyes -"😆", // 1F606 ; fully-qualified # 😆 grinning squinting face -"😅", // 1F605 ; fully-qualified # 😅 grinning face with sweat -"🤣", // 1F923 ; fully-qualified # 🤣 rolling on the floor laughing -"😂", // 1F602 ; fully-qualified # 😂 face with tears of joy -"🙂", // 1F642 ; fully-qualified # 🙂 slightly smiling face -"🙃", // 1F643 ; fully-qualified # 🙃 upside-down face -"😉", // 1F609 ; fully-qualified # 😉 winking face -"😊", // 1F60A ; fully-qualified # 😊 smiling face with smiling eyes -"😇", // 1F607 ; fully-qualified # 😇 smiling face with halo -"🥰", // 1F970 ; fully-qualified # 🥰 smiling face with hearts -"😍", // 1F60D ; fully-qualified # 😍 smiling face with heart-eyes -"🤩", // 1F929 ; fully-qualified # 🤩 star-struck -"😘", // 1F618 ; fully-qualified # 😘 face blowing a kiss -"😗", // 1F617 ; fully-qualified # 😗 kissing face -"😚", // 1F61A ; fully-qualified # 😚 kissing face with closed eyes -"😙", // 1F619 ; fully-qualified # 😙 kissing face with smiling eyes -"😋", // 1F60B ; fully-qualified # 😋 face savoring food -"😛", // 1F61B ; fully-qualified # 😛 face with tongue -"😜", // 1F61C ; fully-qualified # 😜 winking face with tongue -"🤪", // 1F92A ; fully-qualified # 🤪 zany face -"😝", // 1F61D ; fully-qualified # 😝 squinting face with tongue -"🤑", // 1F911 ; fully-qualified # 🤑 money-mouth face -"🤗", // 1F917 ; fully-qualified # 🤗 hugging face -"🤭", // 1F92D ; fully-qualified # 🤭 face with hand over mouth -"🤫", // 1F92B ; fully-qualified # 🤫 shushing face -"🤔", // 1F914 ; fully-qualified # 🤔 thinking face -"🤐", // 1F910 ; fully-qualified # 🤐 zipper-mouth face -"🤨", // 1F928 ; fully-qualified # 🤨 face with raised eyebrow -"😐", // 1F610 ; fully-qualified # 😐 neutral face -"😑", // 1F611 ; fully-qualified # 😑 expressionless face -"😶", // 1F636 ; fully-qualified # 😶 face without mouth -"😏", // 1F60F ; fully-qualified # 😏 smirking face -"😒", // 1F612 ; fully-qualified # 😒 unamused face -"🙄", // 1F644 ; fully-qualified # 🙄 face with rolling eyes -"😬", // 1F62C ; fully-qualified # 😬 grimacing face -"🤥", // 1F925 ; fully-qualified # 🤥 lying face -"😌", // 1F60C ; fully-qualified # 😌 relieved face -"😔", // 1F614 ; fully-qualified # 😔 pensive face -"😪", // 1F62A ; fully-qualified # 😪 sleepy face -"🤤", // 1F924 ; fully-qualified # 🤤 drooling face -"😴", // 1F634 ; fully-qualified # 😴 sleeping face -"😷", // 1F637 ; fully-qualified # 😷 face with medical mask -"🤒", // 1F912 ; fully-qualified # 🤒 face with thermometer -"🤕", // 1F915 ; fully-qualified # 🤕 face with head-bandage -"🤢", // 1F922 ; fully-qualified # 🤢 nauseated face -"🤮", // 1F92E ; fully-qualified # 🤮 face vomiting -"🤧", // 1F927 ; fully-qualified # 🤧 sneezing face -"🥵", // 1F975 ; fully-qualified # 🥵 hot face -"🥶", // 1F976 ; fully-qualified # 🥶 cold face -"🥴", // 1F974 ; fully-qualified # 🥴 woozy face -"😵", // 1F635 ; fully-qualified # 😵 dizzy face -"🤯", // 1F92F ; fully-qualified # 🤯 exploding head -"🤠", // 1F920 ; fully-qualified # 🤠 cowboy hat face -"🥳", // 1F973 ; fully-qualified # 🥳 partying face -"😎", // 1F60E ; fully-qualified # 😎 smiling face with sunglasses -"🤓", // 1F913 ; fully-qualified # 🤓 nerd face -"🧐", // 1F9D0 ; fully-qualified # 🧐 face with monocle -"😕", // 1F615 ; fully-qualified # 😕 confused face -"😟", // 1F61F ; fully-qualified # 😟 worried face -"🙁", // 1F641 ; fully-qualified # 🙁 slightly frowning face -"😮", // 1F62E ; fully-qualified # 😮 face with open mouth -"😯", // 1F62F ; fully-qualified # 😯 hushed face -"😲", // 1F632 ; fully-qualified # 😲 astonished face -"😳", // 1F633 ; fully-qualified # 😳 flushed face -"🥺", // 1F97A ; fully-qualified # 🥺 pleading face -"😦", // 1F626 ; fully-qualified # 😦 frowning face with open mouth -"😧", // 1F627 ; fully-qualified # 😧 anguished face -"😨", // 1F628 ; fully-qualified # 😨 fearful face -"😰", // 1F630 ; fully-qualified # 😰 anxious face with sweat -"😥", // 1F625 ; fully-qualified # 😥 sad but relieved face -"😢", // 1F622 ; fully-qualified # 😢 crying face -"😭", // 1F62D ; fully-qualified # 😭 loudly crying face -"😱", // 1F631 ; fully-qualified # 😱 face screaming in fear -"😖", // 1F616 ; fully-qualified # 😖 confounded face -"😣", // 1F623 ; fully-qualified # 😣 persevering face -"😞", // 1F61E ; fully-qualified # 😞 disappointed face -"😓", // 1F613 ; fully-qualified # 😓 downcast face with sweat -"😩", // 1F629 ; fully-qualified # 😩 weary face -"😫", // 1F62B ; fully-qualified # 😫 tired face -"🥱", // 1F971 ; fully-qualified # 🥱 yawning face -"😤", // 1F624 ; fully-qualified # 😤 face with steam from nose -"😡", // 1F621 ; fully-qualified # 😡 pouting face -"😠", // 1F620 ; fully-qualified # 😠 angry face -"🤬", // 1F92C ; fully-qualified # 🤬 face with symbols on mouth -"😈", // 1F608 ; fully-qualified # 😈 smiling face with horns -"👿", // 1F47F ; fully-qualified # 👿 angry face with horns -"💀", // 1F480 ; fully-qualified # 💀 skull -"💩", // 1F4A9 ; fully-qualified # 💩 pile of poo -"🤡", // 1F921 ; fully-qualified # 🤡 clown face -"👹", // 1F479 ; fully-qualified # 👹 ogre -"👺", // 1F47A ; fully-qualified # 👺 goblin -"👻", // 1F47B ; fully-qualified # 👻 ghost -"👽", // 1F47D ; fully-qualified # 👽 alien -"👾", // 1F47E ; fully-qualified # 👾 alien monster -"🤖", // 1F916 ; fully-qualified # 🤖 robot -"😺", // 1F63A ; fully-qualified # 😺 grinning cat -"😸", // 1F638 ; fully-qualified # 😸 grinning cat with smiling eyes -"😹", // 1F639 ; fully-qualified # 😹 cat with tears of joy -"😻", // 1F63B ; fully-qualified # 😻 smiling cat with heart-eyes -"😼", // 1F63C ; fully-qualified # 😼 cat with wry smile -"😽", // 1F63D ; fully-qualified # 😽 kissing cat -"🙀", // 1F640 ; fully-qualified # 🙀 weary cat -"😿", // 1F63F ; fully-qualified # 😿 crying cat -"😾", // 1F63E ; fully-qualified # 😾 pouting cat -"🙈", // 1F648 ; fully-qualified # 🙈 see-no-evil monkey -"🙉", // 1F649 ; fully-qualified # 🙉 hear-no-evil monkey -"🙊", // 1F64A ; fully-qualified # 🙊 speak-no-evil monkey -"💋", // 1F48B ; fully-qualified # 💋 kiss mark -"💌", // 1F48C ; fully-qualified # 💌 love letter -"💘", // 1F498 ; fully-qualified # 💘 heart with arrow -"💝", // 1F49D ; fully-qualified # 💝 heart with ribbon -"💖", // 1F496 ; fully-qualified # 💖 sparkling heart -"💗", // 1F497 ; fully-qualified # 💗 growing heart -"💓", // 1F493 ; fully-qualified # 💓 beating heart -"💞", // 1F49E ; fully-qualified # 💞 revolving hearts -"💕", // 1F495 ; fully-qualified # 💕 two hearts -"💟", // 1F49F ; fully-qualified # 💟 heart decoration -"💔", // 1F494 ; fully-qualified # 💔 broken heart -"🧡", // 1F9E1 ; fully-qualified # 🧡 orange heart -"💛", // 1F49B ; fully-qualified # 💛 yellow heart -"💚", // 1F49A ; fully-qualified # 💚 green heart -"💙", // 1F499 ; fully-qualified # 💙 blue heart -"💜", // 1F49C ; fully-qualified # 💜 purple heart -"🤎", // 1F90E ; fully-qualified # 🤎 brown heart -"🖤", // 1F5A4 ; fully-qualified # 🖤 black heart -"🤍", // 1F90D ; fully-qualified # 🤍 white heart -"💯", // 1F4AF ; fully-qualified # 💯 hundred points -"💢", // 1F4A2 ; fully-qualified # 💢 anger symbol -"💥", // 1F4A5 ; fully-qualified # 💥 collision -"💫", // 1F4AB ; fully-qualified # 💫 dizzy -"💦", // 1F4A6 ; fully-qualified # 💦 sweat droplets -"💨", // 1F4A8 ; fully-qualified # 💨 dashing away -"💣", // 1F4A3 ; fully-qualified # 💣 bomb -"💬", // 1F4AC ; fully-qualified # 💬 speech balloon -"💭", // 1F4AD ; fully-qualified # 💭 thought balloon -"💤", // 1F4A4 ; fully-qualified # 💤 zzz -}, -{ // # group: People & Body -"👋", // 1F44B ; fully-qualified # 👋 waving hand -"🤚", // 1F91A ; fully-qualified # 🤚 raised back of hand -"✋", // 270B ; fully-qualified # ✋ raised hand -"🖖", // 1F596 ; fully-qualified # 🖖 vulcan salute -"👌", // 1F44C ; fully-qualified # 👌 OK hand -"🤏", // 1F90F ; fully-qualified # 🤏 pinching hand -"🤞", // 1F91E ; fully-qualified # 🤞 crossed fingers -"🤟", // 1F91F ; fully-qualified # 🤟 love-you gesture -"🤘", // 1F918 ; fully-qualified # 🤘 sign of the horns -"🤙", // 1F919 ; fully-qualified # 🤙 call me hand -"👈", // 1F448 ; fully-qualified # 👈 backhand index pointing left -"👉", // 1F449 ; fully-qualified # 👉 backhand index pointing right -"👆", // 1F446 ; fully-qualified # 👆 backhand index pointing up -"🖕", // 1F595 ; fully-qualified # 🖕 middle finger -"👇", // 1F447 ; fully-qualified # 👇 backhand index pointing down -"👍", // 1F44D ; fully-qualified # 👍 thumbs up -"👎", // 1F44E ; fully-qualified # 👎 thumbs down -"✊", // 270A ; fully-qualified # ✊ raised fist -"👊", // 1F44A ; fully-qualified # 👊 oncoming fist -"🤛", // 1F91B ; fully-qualified # 🤛 left-facing fist -"🤜", // 1F91C ; fully-qualified # 🤜 right-facing fist -"👏", // 1F44F ; fully-qualified # 👏 clapping hands -"🙌", // 1F64C ; fully-qualified # 🙌 raising hands -"👐", // 1F450 ; fully-qualified # 👐 open hands -"🤲", // 1F932 ; fully-qualified # 🤲 palms up together -"🤝", // 1F91D ; fully-qualified # 🤝 handshake -"🙏", // 1F64F ; fully-qualified # 🙏 folded hands -"💅", // 1F485 ; fully-qualified # 💅 nail polish -"🤳", // 1F933 ; fully-qualified # 🤳 selfie -"💪", // 1F4AA ; fully-qualified # 💪 flexed biceps -"🦾", // 1F9BE ; fully-qualified # 🦾 mechanical arm -"🦿", // 1F9BF ; fully-qualified # 🦿 mechanical leg -"🦵", // 1F9B5 ; fully-qualified # 🦵 leg -"🦶", // 1F9B6 ; fully-qualified # 🦶 foot -"👂", // 1F442 ; fully-qualified # 👂 ear -"🦻", // 1F9BB ; fully-qualified # 🦻 ear with hearing aid -"👃", // 1F443 ; fully-qualified # 👃 nose -"🧠", // 1F9E0 ; fully-qualified # 🧠 brain -"🦷", // 1F9B7 ; fully-qualified # 🦷 tooth -"🦴", // 1F9B4 ; fully-qualified # 🦴 bone -"👀", // 1F440 ; fully-qualified # 👀 eyes -"👅", // 1F445 ; fully-qualified # 👅 tongue -"👄", // 1F444 ; fully-qualified # 👄 mouth -"👶", // 1F476 ; fully-qualified # 👶 baby -"🧒", // 1F9D2 ; fully-qualified # 🧒 child -"👦", // 1F466 ; fully-qualified # 👦 boy -"👧", // 1F467 ; fully-qualified # 👧 girl -"🧑", // 1F9D1 ; fully-qualified # 🧑 person -"👱", // 1F471 ; fully-qualified # 👱 person: blond hair -"👨", // 1F468 ; fully-qualified # 👨 man -"🧔", // 1F9D4 ; fully-qualified # 🧔 man: beard -"👩", // 1F469 ; fully-qualified # 👩 woman -"🧓", // 1F9D3 ; fully-qualified # 🧓 older person -"👴", // 1F474 ; fully-qualified # 👴 old man -"👵", // 1F475 ; fully-qualified # 👵 old woman -"🙍", // 1F64D ; fully-qualified # 🙍 person frowning -"🙎", // 1F64E ; fully-qualified # 🙎 person pouting -"🙅", // 1F645 ; fully-qualified # 🙅 person gesturing NO -"🙆", // 1F646 ; fully-qualified # 🙆 person gesturing OK -"💁", // 1F481 ; fully-qualified # 💁 person tipping hand -"🙋", // 1F64B ; fully-qualified # 🙋 person raising hand -"🧏", // 1F9CF ; fully-qualified # 🧏 deaf person -"🙇", // 1F647 ; fully-qualified # 🙇 person bowing -"🤦", // 1F926 ; fully-qualified # 🤦 person facepalming -"🤷", // 1F937 ; fully-qualified # 🤷 person shrugging -"👮", // 1F46E ; fully-qualified # 👮 police officer -"💂", // 1F482 ; fully-qualified # 💂 guard -"👷", // 1F477 ; fully-qualified # 👷 construction worker -"🤴", // 1F934 ; fully-qualified # 🤴 prince -"👸", // 1F478 ; fully-qualified # 👸 princess -"👳", // 1F473 ; fully-qualified # 👳 person wearing turban -"👲", // 1F472 ; fully-qualified # 👲 man with Chinese cap -"🧕", // 1F9D5 ; fully-qualified # 🧕 woman with headscarf -"🤵", // 1F935 ; fully-qualified # 🤵 man in tuxedo -"👰", // 1F470 ; fully-qualified # 👰 bride with veil -"🤰", // 1F930 ; fully-qualified # 🤰 pregnant woman -"🤱", // 1F931 ; fully-qualified # 🤱 breast-feeding -"👼", // 1F47C ; fully-qualified # 👼 baby angel -"🎅", // 1F385 ; fully-qualified # 🎅 Santa Claus -"🤶", // 1F936 ; fully-qualified # 🤶 Mrs. Claus -"🦸", // 1F9B8 ; fully-qualified # 🦸 superhero -"🦹", // 1F9B9 ; fully-qualified # 🦹 supervillain -"🧙", // 1F9D9 ; fully-qualified # 🧙 mage -"🧚", // 1F9DA ; fully-qualified # 🧚 fairy -"🧛", // 1F9DB ; fully-qualified # 🧛 vampire -"🧜", // 1F9DC ; fully-qualified # 🧜 merperson -"🧝", // 1F9DD ; fully-qualified # 🧝 elf -"🧞", // 1F9DE ; fully-qualified # 🧞 genie -"🧟", // 1F9DF ; fully-qualified # 🧟 zombie -"💆", // 1F486 ; fully-qualified # 💆 person getting massage -"💇", // 1F487 ; fully-qualified # 💇 person getting haircut -"🚶", // 1F6B6 ; fully-qualified # 🚶 person walking -"🧍", // 1F9CD ; fully-qualified # 🧍 person standing -"🧎", // 1F9CE ; fully-qualified # 🧎 person kneeling -"🏃", // 1F3C3 ; fully-qualified # 🏃 person running -"💃", // 1F483 ; fully-qualified # 💃 woman dancing -"🕺", // 1F57A ; fully-qualified # 🕺 man dancing -"👯", // 1F46F ; fully-qualified # 👯 people with bunny ears -"🧖", // 1F9D6 ; fully-qualified # 🧖 person in steamy room -"🧗", // 1F9D7 ; fully-qualified # 🧗 person climbing -"🤺", // 1F93A ; fully-qualified # 🤺 person fencing -"🏇", // 1F3C7 ; fully-qualified # 🏇 horse racing -"🏂", // 1F3C2 ; fully-qualified # 🏂 snowboarder -"🏄", // 1F3C4 ; fully-qualified # 🏄 person surfing -"🚣", // 1F6A3 ; fully-qualified # 🚣 person rowing boat -"🏊", // 1F3CA ; fully-qualified # 🏊 person swimming -"🚴", // 1F6B4 ; fully-qualified # 🚴 person biking -"🚵", // 1F6B5 ; fully-qualified # 🚵 person mountain biking -"🤸", // 1F938 ; fully-qualified # 🤸 person cartwheeling -"🤼", // 1F93C ; fully-qualified # 🤼 people wrestling -"🤽", // 1F93D ; fully-qualified # 🤽 person playing water polo -"🤾", // 1F93E ; fully-qualified # 🤾 person playing handball -"🤹", // 1F939 ; fully-qualified # 🤹 person juggling -"🧘", // 1F9D8 ; fully-qualified # 🧘 person in lotus position -"🛀", // 1F6C0 ; fully-qualified # 🛀 person taking bath -"🛌", // 1F6CC ; fully-qualified # 🛌 person in bed -"👭", // 1F46D ; fully-qualified # 👭 women holding hands -"👫", // 1F46B ; fully-qualified # 👫 woman and man holding hands -"👬", // 1F46C ; fully-qualified # 👬 men holding hands -"💏", // 1F48F ; fully-qualified # 💏 kiss -"💑", // 1F491 ; fully-qualified # 💑 couple with heart -"👪", // 1F46A ; fully-qualified # 👪 family -"👤", // 1F464 ; fully-qualified # 👤 bust in silhouette -"👥", // 1F465 ; fully-qualified # 👥 busts in silhouette -"👣", // 1F463 ; fully-qualified # 👣 footprints -}, -{ // # group: Animals & Nature -"🐵", // 1F435 ; fully-qualified # 🐵 monkey face -"🐒", // 1F412 ; fully-qualified # 🐒 monkey -"🦍", // 1F98D ; fully-qualified # 🦍 gorilla -"🦧", // 1F9A7 ; fully-qualified # 🦧 orangutan -"🐶", // 1F436 ; fully-qualified # 🐶 dog face -"🐕", // 1F415 ; fully-qualified # 🐕 dog -"🦮", // 1F9AE ; fully-qualified # 🦮 guide dog -"🐩", // 1F429 ; fully-qualified # 🐩 poodle -"🐺", // 1F43A ; fully-qualified # 🐺 wolf -"🦊", // 1F98A ; fully-qualified # 🦊 fox -"🦝", // 1F99D ; fully-qualified # 🦝 raccoon -"🐱", // 1F431 ; fully-qualified # 🐱 cat face -"🐈", // 1F408 ; fully-qualified # 🐈 cat -"🦁", // 1F981 ; fully-qualified # 🦁 lion -"🐯", // 1F42F ; fully-qualified # 🐯 tiger face -"🐅", // 1F405 ; fully-qualified # 🐅 tiger -"🐆", // 1F406 ; fully-qualified # 🐆 leopard -"🐴", // 1F434 ; fully-qualified # 🐴 horse face -"🐎", // 1F40E ; fully-qualified # 🐎 horse -"🦄", // 1F984 ; fully-qualified # 🦄 unicorn -"🦓", // 1F993 ; fully-qualified # 🦓 zebra -"🦌", // 1F98C ; fully-qualified # 🦌 deer -"🐮", // 1F42E ; fully-qualified # 🐮 cow face -"🐂", // 1F402 ; fully-qualified # 🐂 ox -"🐃", // 1F403 ; fully-qualified # 🐃 water buffalo -"🐄", // 1F404 ; fully-qualified # 🐄 cow -"🐷", // 1F437 ; fully-qualified # 🐷 pig face -"🐖", // 1F416 ; fully-qualified # 🐖 pig -"🐗", // 1F417 ; fully-qualified # 🐗 boar -"🐽", // 1F43D ; fully-qualified # 🐽 pig nose -"🐏", // 1F40F ; fully-qualified # 🐏 ram -"🐑", // 1F411 ; fully-qualified # 🐑 ewe -"🐐", // 1F410 ; fully-qualified # 🐐 goat -"🐪", // 1F42A ; fully-qualified # 🐪 camel -"🐫", // 1F42B ; fully-qualified # 🐫 two-hump camel -"🦙", // 1F999 ; fully-qualified # 🦙 llama -"🦒", // 1F992 ; fully-qualified # 🦒 giraffe -"🐘", // 1F418 ; fully-qualified # 🐘 elephant -"🦏", // 1F98F ; fully-qualified # 🦏 rhinoceros -"🦛", // 1F99B ; fully-qualified # 🦛 hippopotamus -"🐭", // 1F42D ; fully-qualified # 🐭 mouse face -"🐁", // 1F401 ; fully-qualified # 🐁 mouse -"🐀", // 1F400 ; fully-qualified # 🐀 rat -"🐹", // 1F439 ; fully-qualified # 🐹 hamster -"🐰", // 1F430 ; fully-qualified # 🐰 rabbit face -"🐇", // 1F407 ; fully-qualified # 🐇 rabbit -"🦔", // 1F994 ; fully-qualified # 🦔 hedgehog -"🦇", // 1F987 ; fully-qualified # 🦇 bat -"🐻", // 1F43B ; fully-qualified # 🐻 bear -"🐨", // 1F428 ; fully-qualified # 🐨 koala -"🐼", // 1F43C ; fully-qualified # 🐼 panda -"🦥", // 1F9A5 ; fully-qualified # 🦥 sloth -"🦦", // 1F9A6 ; fully-qualified # 🦦 otter -"🦨", // 1F9A8 ; fully-qualified # 🦨 skunk -"🦘", // 1F998 ; fully-qualified # 🦘 kangaroo -"🦡", // 1F9A1 ; fully-qualified # 🦡 badger -"🐾", // 1F43E ; fully-qualified # 🐾 paw prints -"🦃", // 1F983 ; fully-qualified # 🦃 turkey -"🐔", // 1F414 ; fully-qualified # 🐔 chicken -"🐓", // 1F413 ; fully-qualified # 🐓 rooster -"🐣", // 1F423 ; fully-qualified # 🐣 hatching chick -"🐤", // 1F424 ; fully-qualified # 🐤 baby chick -"🐥", // 1F425 ; fully-qualified # 🐥 front-facing baby chick -"🐦", // 1F426 ; fully-qualified # 🐦 bird -"🐧", // 1F427 ; fully-qualified # 🐧 penguin -"🦅", // 1F985 ; fully-qualified # 🦅 eagle -"🦆", // 1F986 ; fully-qualified # 🦆 duck -"🦢", // 1F9A2 ; fully-qualified # 🦢 swan -"🦉", // 1F989 ; fully-qualified # 🦉 owl -"🦩", // 1F9A9 ; fully-qualified # 🦩 flamingo -"🦚", // 1F99A ; fully-qualified # 🦚 peacock -"🦜", // 1F99C ; fully-qualified # 🦜 parrot -"🐸", // 1F438 ; fully-qualified # 🐸 frog -"🐊", // 1F40A ; fully-qualified # 🐊 crocodile -"🐢", // 1F422 ; fully-qualified # 🐢 turtle -"🦎", // 1F98E ; fully-qualified # 🦎 lizard -"🐍", // 1F40D ; fully-qualified # 🐍 snake -"🐲", // 1F432 ; fully-qualified # 🐲 dragon face -"🐉", // 1F409 ; fully-qualified # 🐉 dragon -"🦕", // 1F995 ; fully-qualified # 🦕 sauropod -"🦖", // 1F996 ; fully-qualified # 🦖 T-Rex -"🐳", // 1F433 ; fully-qualified # 🐳 spouting whale -"🐋", // 1F40B ; fully-qualified # 🐋 whale -"🐬", // 1F42C ; fully-qualified # 🐬 dolphin -"🐟", // 1F41F ; fully-qualified # 🐟 fish -"🐠", // 1F420 ; fully-qualified # 🐠 tropical fish -"🐡", // 1F421 ; fully-qualified # 🐡 blowfish -"🦈", // 1F988 ; fully-qualified # 🦈 shark -"🐙", // 1F419 ; fully-qualified # 🐙 octopus -"🐚", // 1F41A ; fully-qualified # 🐚 spiral shell -"🐌", // 1F40C ; fully-qualified # 🐌 snail -"🦋", // 1F98B ; fully-qualified # 🦋 butterfly -"🐛", // 1F41B ; fully-qualified # 🐛 bug -"🐜", // 1F41C ; fully-qualified # 🐜 ant -"🐝", // 1F41D ; fully-qualified # 🐝 honeybee -"🐞", // 1F41E ; fully-qualified # 🐞 lady beetle -"🦗", // 1F997 ; fully-qualified # 🦗 cricket -"🦂", // 1F982 ; fully-qualified # 🦂 scorpion -"🦟", // 1F99F ; fully-qualified # 🦟 mosquito -"🦠", // 1F9A0 ; fully-qualified # 🦠 microbe -"💐", // 1F490 ; fully-qualified # 💐 bouquet -"🌸", // 1F338 ; fully-qualified # 🌸 cherry blossom -"💮", // 1F4AE ; fully-qualified # 💮 white flower -"🌹", // 1F339 ; fully-qualified # 🌹 rose -"🥀", // 1F940 ; fully-qualified # 🥀 wilted flower -"🌺", // 1F33A ; fully-qualified # 🌺 hibiscus -"🌻", // 1F33B ; fully-qualified # 🌻 sunflower -"🌼", // 1F33C ; fully-qualified # 🌼 blossom -"🌷", // 1F337 ; fully-qualified # 🌷 tulip -"🌱", // 1F331 ; fully-qualified # 🌱 seedling -"🌲", // 1F332 ; fully-qualified # 🌲 evergreen tree -"🌳", // 1F333 ; fully-qualified # 🌳 deciduous tree -"🌴", // 1F334 ; fully-qualified # 🌴 palm tree -"🌵", // 1F335 ; fully-qualified # 🌵 cactus -"🌾", // 1F33E ; fully-qualified # 🌾 sheaf of rice -"🌿", // 1F33F ; fully-qualified # 🌿 herb -"🍀", // 1F340 ; fully-qualified # 🍀 four leaf clover -"🍁", // 1F341 ; fully-qualified # 🍁 maple leaf -"🍂", // 1F342 ; fully-qualified # 🍂 fallen leaf -"🍃", // 1F343 ; fully-qualified # 🍃 leaf fluttering in wind -}, -{ // # group: Food & Drink -"🍇", // 1F347 ; fully-qualified # 🍇 grapes -"🍈", // 1F348 ; fully-qualified # 🍈 melon -"🍉", // 1F349 ; fully-qualified # 🍉 watermelon -"🍊", // 1F34A ; fully-qualified # 🍊 tangerine -"🍋", // 1F34B ; fully-qualified # 🍋 lemon -"🍌", // 1F34C ; fully-qualified # 🍌 banana -"🍍", // 1F34D ; fully-qualified # 🍍 pineapple -"🥭", // 1F96D ; fully-qualified # 🥭 mango -"🍎", // 1F34E ; fully-qualified # 🍎 red apple -"🍏", // 1F34F ; fully-qualified # 🍏 green apple -"🍐", // 1F350 ; fully-qualified # 🍐 pear -"🍑", // 1F351 ; fully-qualified # 🍑 peach -"🍒", // 1F352 ; fully-qualified # 🍒 cherries -"🍓", // 1F353 ; fully-qualified # 🍓 strawberry -"🥝", // 1F95D ; fully-qualified # 🥝 kiwi fruit -"🍅", // 1F345 ; fully-qualified # 🍅 tomato -"🥥", // 1F965 ; fully-qualified # 🥥 coconut -"🥑", // 1F951 ; fully-qualified # 🥑 avocado -"🍆", // 1F346 ; fully-qualified # 🍆 eggplant -"🥔", // 1F954 ; fully-qualified # 🥔 potato -"🥕", // 1F955 ; fully-qualified # 🥕 carrot -"🌽", // 1F33D ; fully-qualified # 🌽 ear of corn -"🥒", // 1F952 ; fully-qualified # 🥒 cucumber -"🥬", // 1F96C ; fully-qualified # 🥬 leafy green -"🥦", // 1F966 ; fully-qualified # 🥦 broccoli -"🧄", // 1F9C4 ; fully-qualified # 🧄 garlic -"🧅", // 1F9C5 ; fully-qualified # 🧅 onion -"🍄", // 1F344 ; fully-qualified # 🍄 mushroom -"🥜", // 1F95C ; fully-qualified # 🥜 peanuts -"🌰", // 1F330 ; fully-qualified # 🌰 chestnut -"🍞", // 1F35E ; fully-qualified # 🍞 bread -"🥐", // 1F950 ; fully-qualified # 🥐 croissant -"🥖", // 1F956 ; fully-qualified # 🥖 baguette bread -"🥨", // 1F968 ; fully-qualified # 🥨 pretzel -"🥯", // 1F96F ; fully-qualified # 🥯 bagel -"🥞", // 1F95E ; fully-qualified # 🥞 pancakes -"🧇", // 1F9C7 ; fully-qualified # 🧇 waffle -"🧀", // 1F9C0 ; fully-qualified # 🧀 cheese wedge -"🍖", // 1F356 ; fully-qualified # 🍖 meat on bone -"🍗", // 1F357 ; fully-qualified # 🍗 poultry leg -"🥩", // 1F969 ; fully-qualified # 🥩 cut of meat -"🥓", // 1F953 ; fully-qualified # 🥓 bacon -"🍔", // 1F354 ; fully-qualified # 🍔 hamburger -"🍟", // 1F35F ; fully-qualified # 🍟 french fries -"🍕", // 1F355 ; fully-qualified # 🍕 pizza -"🌭", // 1F32D ; fully-qualified # 🌭 hot dog -"🥪", // 1F96A ; fully-qualified # 🥪 sandwich -"🌮", // 1F32E ; fully-qualified # 🌮 taco -"🌯", // 1F32F ; fully-qualified # 🌯 burrito -"🥙", // 1F959 ; fully-qualified # 🥙 stuffed flatbread -"🧆", // 1F9C6 ; fully-qualified # 🧆 falafel -"🥚", // 1F95A ; fully-qualified # 🥚 egg -"🍳", // 1F373 ; fully-qualified # 🍳 cooking -"🥘", // 1F958 ; fully-qualified # 🥘 shallow pan of food -"🍲", // 1F372 ; fully-qualified # 🍲 pot of food -"🥣", // 1F963 ; fully-qualified # 🥣 bowl with spoon -"🥗", // 1F957 ; fully-qualified # 🥗 green salad -"🍿", // 1F37F ; fully-qualified # 🍿 popcorn -"🧈", // 1F9C8 ; fully-qualified # 🧈 butter -"🧂", // 1F9C2 ; fully-qualified # 🧂 salt -"🥫", // 1F96B ; fully-qualified # 🥫 canned food -"🍱", // 1F371 ; fully-qualified # 🍱 bento box -"🍘", // 1F358 ; fully-qualified # 🍘 rice cracker -"🍙", // 1F359 ; fully-qualified # 🍙 rice ball -"🍚", // 1F35A ; fully-qualified # 🍚 cooked rice -"🍛", // 1F35B ; fully-qualified # 🍛 curry rice -"🍜", // 1F35C ; fully-qualified # 🍜 steaming bowl -"🍝", // 1F35D ; fully-qualified # 🍝 spaghetti -"🍠", // 1F360 ; fully-qualified # 🍠 roasted sweet potato -"🍢", // 1F362 ; fully-qualified # 🍢 oden -"🍣", // 1F363 ; fully-qualified # 🍣 sushi -"🍤", // 1F364 ; fully-qualified # 🍤 fried shrimp -"🍥", // 1F365 ; fully-qualified # 🍥 fish cake with swirl -"🥮", // 1F96E ; fully-qualified # 🥮 moon cake -"🍡", // 1F361 ; fully-qualified # 🍡 dango -"🥟", // 1F95F ; fully-qualified # 🥟 dumpling -"🥠", // 1F960 ; fully-qualified # 🥠 fortune cookie -"🥡", // 1F961 ; fully-qualified # 🥡 takeout box -"🦀", // 1F980 ; fully-qualified # 🦀 crab -"🦞", // 1F99E ; fully-qualified # 🦞 lobster -"🦐", // 1F990 ; fully-qualified # 🦐 shrimp -"🦑", // 1F991 ; fully-qualified # 🦑 squid -"🦪", // 1F9AA ; fully-qualified # 🦪 oyster -"🍦", // 1F366 ; fully-qualified # 🍦 soft ice cream -"🍧", // 1F367 ; fully-qualified # 🍧 shaved ice -"🍨", // 1F368 ; fully-qualified # 🍨 ice cream -"🍩", // 1F369 ; fully-qualified # 🍩 doughnut -"🍪", // 1F36A ; fully-qualified # 🍪 cookie -"🎂", // 1F382 ; fully-qualified # 🎂 birthday cake -"🍰", // 1F370 ; fully-qualified # 🍰 shortcake -"🧁", // 1F9C1 ; fully-qualified # 🧁 cupcake -"🥧", // 1F967 ; fully-qualified # 🥧 pie -"🍫", // 1F36B ; fully-qualified # 🍫 chocolate bar -"🍬", // 1F36C ; fully-qualified # 🍬 candy -"🍭", // 1F36D ; fully-qualified # 🍭 lollipop -"🍮", // 1F36E ; fully-qualified # 🍮 custard -"🍯", // 1F36F ; fully-qualified # 🍯 honey pot -"🍼", // 1F37C ; fully-qualified # 🍼 baby bottle -"🥛", // 1F95B ; fully-qualified # 🥛 glass of milk -"☕", // 2615 ; fully-qualified # ☕ hot beverage -"🍵", // 1F375 ; fully-qualified # 🍵 teacup without handle -"🍶", // 1F376 ; fully-qualified # 🍶 sake -"🍾", // 1F37E ; fully-qualified # 🍾 bottle with popping cork -"🍷", // 1F377 ; fully-qualified # 🍷 wine glass -"🍸", // 1F378 ; fully-qualified # 🍸 cocktail glass -"🍹", // 1F379 ; fully-qualified # 🍹 tropical drink -"🍺", // 1F37A ; fully-qualified # 🍺 beer mug -"🍻", // 1F37B ; fully-qualified # 🍻 clinking beer mugs -"🥂", // 1F942 ; fully-qualified # 🥂 clinking glasses -"🥃", // 1F943 ; fully-qualified # 🥃 tumbler glass -"🥤", // 1F964 ; fully-qualified # 🥤 cup with straw -"🧃", // 1F9C3 ; fully-qualified # 🧃 beverage box -"🧉", // 1F9C9 ; fully-qualified # 🧉 mate -"🧊", // 1F9CA ; fully-qualified # 🧊 ice cube -"🥢", // 1F962 ; fully-qualified # 🥢 chopsticks -"🍴", // 1F374 ; fully-qualified # 🍴 fork and knife -"🥄", // 1F944 ; fully-qualified # 🥄 spoon -"🔪", // 1F52A ; fully-qualified # 🔪 kitchen knife -"🏺", // 1F3FA ; fully-qualified # 🏺 amphora -}, -{ // # group: Travel & Places -"🌍", // 1F30D ; fully-qualified # 🌍 globe showing Europe-Africa -"🌎", // 1F30E ; fully-qualified # 🌎 globe showing Americas -"🌏", // 1F30F ; fully-qualified # 🌏 globe showing Asia-Australia -"🌐", // 1F310 ; fully-qualified # 🌐 globe with meridians -"🗾", // 1F5FE ; fully-qualified # 🗾 map of Japan -"🧭", // 1F9ED ; fully-qualified # 🧭 compass -"🌋", // 1F30B ; fully-qualified # 🌋 volcano -"🗻", // 1F5FB ; fully-qualified # 🗻 mount fuji -"🧱", // 1F9F1 ; fully-qualified # 🧱 brick -"🏠", // 1F3E0 ; fully-qualified # 🏠 house -"🏡", // 1F3E1 ; fully-qualified # 🏡 house with garden -"🏢", // 1F3E2 ; fully-qualified # 🏢 office building -"🏣", // 1F3E3 ; fully-qualified # 🏣 Japanese post office -"🏤", // 1F3E4 ; fully-qualified # 🏤 post office -"🏥", // 1F3E5 ; fully-qualified # 🏥 hospital -"🏦", // 1F3E6 ; fully-qualified # 🏦 bank -"🏨", // 1F3E8 ; fully-qualified # 🏨 hotel -"🏩", // 1F3E9 ; fully-qualified # 🏩 love hotel -"🏪", // 1F3EA ; fully-qualified # 🏪 convenience store -"🏫", // 1F3EB ; fully-qualified # 🏫 school -"🏬", // 1F3EC ; fully-qualified # 🏬 department store -"🏭", // 1F3ED ; fully-qualified # 🏭 factory -"🏯", // 1F3EF ; fully-qualified # 🏯 Japanese castle -"🏰", // 1F3F0 ; fully-qualified # 🏰 castle -"💒", // 1F492 ; fully-qualified # 💒 wedding -"🗼", // 1F5FC ; fully-qualified # 🗼 Tokyo tower -"🗽", // 1F5FD ; fully-qualified # 🗽 Statue of Liberty -"⛪", // 26EA ; fully-qualified # ⛪ church -"🕌", // 1F54C ; fully-qualified # 🕌 mosque -"🛕", // 1F6D5 ; fully-qualified # 🛕 hindu temple -"🕍", // 1F54D ; fully-qualified # 🕍 synagogue -"🕋", // 1F54B ; fully-qualified # 🕋 kaaba -"⛲", // 26F2 ; fully-qualified # ⛲ fountain -"⛺", // 26FA ; fully-qualified # ⛺ tent -"🌁", // 1F301 ; fully-qualified # 🌁 foggy -"🌃", // 1F303 ; fully-qualified # 🌃 night with stars -"🌄", // 1F304 ; fully-qualified # 🌄 sunrise over mountains -"🌅", // 1F305 ; fully-qualified # 🌅 sunrise -"🌆", // 1F306 ; fully-qualified # 🌆 cityscape at dusk -"🌇", // 1F307 ; fully-qualified # 🌇 sunset -"🌉", // 1F309 ; fully-qualified # 🌉 bridge at night -"🎠", // 1F3A0 ; fully-qualified # 🎠 carousel horse -"🎡", // 1F3A1 ; fully-qualified # 🎡 ferris wheel -"🎢", // 1F3A2 ; fully-qualified # 🎢 roller coaster -"💈", // 1F488 ; fully-qualified # 💈 barber pole -"🎪", // 1F3AA ; fully-qualified # 🎪 circus tent -"🚂", // 1F682 ; fully-qualified # 🚂 locomotive -"🚃", // 1F683 ; fully-qualified # 🚃 railway car -"🚄", // 1F684 ; fully-qualified # 🚄 high-speed train -"🚅", // 1F685 ; fully-qualified # 🚅 bullet train -"🚆", // 1F686 ; fully-qualified # 🚆 train -"🚇", // 1F687 ; fully-qualified # 🚇 metro -"🚈", // 1F688 ; fully-qualified # 🚈 light rail -"🚉", // 1F689 ; fully-qualified # 🚉 station -"🚊", // 1F68A ; fully-qualified # 🚊 tram -"🚝", // 1F69D ; fully-qualified # 🚝 monorail -"🚞", // 1F69E ; fully-qualified # 🚞 mountain railway -"🚋", // 1F68B ; fully-qualified # 🚋 tram car -"🚌", // 1F68C ; fully-qualified # 🚌 bus -"🚍", // 1F68D ; fully-qualified # 🚍 oncoming bus -"🚎", // 1F68E ; fully-qualified # 🚎 trolleybus -"🚐", // 1F690 ; fully-qualified # 🚐 minibus -"🚑", // 1F691 ; fully-qualified # 🚑 ambulance -"🚒", // 1F692 ; fully-qualified # 🚒 fire engine -"🚓", // 1F693 ; fully-qualified # 🚓 police car -"🚔", // 1F694 ; fully-qualified # 🚔 oncoming police car -"🚕", // 1F695 ; fully-qualified # 🚕 taxi -"🚖", // 1F696 ; fully-qualified # 🚖 oncoming taxi -"🚗", // 1F697 ; fully-qualified # 🚗 automobile -"🚘", // 1F698 ; fully-qualified # 🚘 oncoming automobile -"🚙", // 1F699 ; fully-qualified # 🚙 sport utility vehicle -"🚚", // 1F69A ; fully-qualified # 🚚 delivery truck -"🚛", // 1F69B ; fully-qualified # 🚛 articulated lorry -"🚜", // 1F69C ; fully-qualified # 🚜 tractor -"🛵", // 1F6F5 ; fully-qualified # 🛵 motor scooter -"🦽", // 1F9BD ; fully-qualified # 🦽 manual wheelchair -"🦼", // 1F9BC ; fully-qualified # 🦼 motorized wheelchair -"🛺", // 1F6FA ; fully-qualified # 🛺 auto rickshaw -"🚲", // 1F6B2 ; fully-qualified # 🚲 bicycle -"🛴", // 1F6F4 ; fully-qualified # 🛴 kick scooter -"🛹", // 1F6F9 ; fully-qualified # 🛹 skateboard -"🚏", // 1F68F ; fully-qualified # 🚏 bus stop -"⛽", // 26FD ; fully-qualified # ⛽ fuel pump -"🚨", // 1F6A8 ; fully-qualified # 🚨 police car light -"🚥", // 1F6A5 ; fully-qualified # 🚥 horizontal traffic light -"🚦", // 1F6A6 ; fully-qualified # 🚦 vertical traffic light -"🛑", // 1F6D1 ; fully-qualified # 🛑 stop sign -"🚧", // 1F6A7 ; fully-qualified # 🚧 construction -"⚓", // 2693 ; fully-qualified # ⚓ anchor -"⛵", // 26F5 ; fully-qualified # ⛵ sailboat -"🛶", // 1F6F6 ; fully-qualified # 🛶 canoe -"🚤", // 1F6A4 ; fully-qualified # 🚤 speedboat -"🚢", // 1F6A2 ; fully-qualified # 🚢 ship -"🛫", // 1F6EB ; fully-qualified # 🛫 airplane departure -"🛬", // 1F6EC ; fully-qualified # 🛬 airplane arrival -"🪂", // 1FA82 ; fully-qualified # 🪂 parachute -"💺", // 1F4BA ; fully-qualified # 💺 seat -"🚁", // 1F681 ; fully-qualified # 🚁 helicopter -"🚟", // 1F69F ; fully-qualified # 🚟 suspension railway -"🚠", // 1F6A0 ; fully-qualified # 🚠 mountain cableway -"🚡", // 1F6A1 ; fully-qualified # 🚡 aerial tramway -"🚀", // 1F680 ; fully-qualified # 🚀 rocket -"🛸", // 1F6F8 ; fully-qualified # 🛸 flying saucer -"🧳", // 1F9F3 ; fully-qualified # 🧳 luggage -"⌛", // 231B ; fully-qualified # ⌛ hourglass done -"⏳", // 23F3 ; fully-qualified # ⏳ hourglass not done -"⌚", // 231A ; fully-qualified # ⌚ watch -"⏰", // 23F0 ; fully-qualified # ⏰ alarm clock -"🕛", // 1F55B ; fully-qualified # 🕛 twelve o’clock -"🕧", // 1F567 ; fully-qualified # 🕧 twelve-thirty -"🕐", // 1F550 ; fully-qualified # 🕐 one o’clock -"🕜", // 1F55C ; fully-qualified # 🕜 one-thirty -"🕑", // 1F551 ; fully-qualified # 🕑 two o’clock -"🕝", // 1F55D ; fully-qualified # 🕝 two-thirty -"🕒", // 1F552 ; fully-qualified # 🕒 three o’clock -"🕞", // 1F55E ; fully-qualified # 🕞 three-thirty -"🕓", // 1F553 ; fully-qualified # 🕓 four o’clock -"🕟", // 1F55F ; fully-qualified # 🕟 four-thirty -"🕔", // 1F554 ; fully-qualified # 🕔 five o’clock -"🕠", // 1F560 ; fully-qualified # 🕠 five-thirty -"🕕", // 1F555 ; fully-qualified # 🕕 six o’clock -"🕡", // 1F561 ; fully-qualified # 🕡 six-thirty -"🕖", // 1F556 ; fully-qualified # 🕖 seven o’clock -"🕢", // 1F562 ; fully-qualified # 🕢 seven-thirty -"🕗", // 1F557 ; fully-qualified # 🕗 eight o’clock -"🕣", // 1F563 ; fully-qualified # 🕣 eight-thirty -"🕘", // 1F558 ; fully-qualified # 🕘 nine o’clock -"🕤", // 1F564 ; fully-qualified # 🕤 nine-thirty -"🕙", // 1F559 ; fully-qualified # 🕙 ten o’clock -"🕥", // 1F565 ; fully-qualified # 🕥 ten-thirty -"🕚", // 1F55A ; fully-qualified # 🕚 eleven o’clock -"🕦", // 1F566 ; fully-qualified # 🕦 eleven-thirty -"🌑", // 1F311 ; fully-qualified # 🌑 new moon -"🌒", // 1F312 ; fully-qualified # 🌒 waxing crescent moon -"🌓", // 1F313 ; fully-qualified # 🌓 first quarter moon -"🌔", // 1F314 ; fully-qualified # 🌔 waxing gibbous moon -"🌕", // 1F315 ; fully-qualified # 🌕 full moon -"🌖", // 1F316 ; fully-qualified # 🌖 waning gibbous moon -"🌗", // 1F317 ; fully-qualified # 🌗 last quarter moon -"🌘", // 1F318 ; fully-qualified # 🌘 waning crescent moon -"🌙", // 1F319 ; fully-qualified # 🌙 crescent moon -"🌚", // 1F31A ; fully-qualified # 🌚 new moon face -"🌛", // 1F31B ; fully-qualified # 🌛 first quarter moon face -"🌜", // 1F31C ; fully-qualified # 🌜 last quarter moon face -"🌝", // 1F31D ; fully-qualified # 🌝 full moon face -"🌞", // 1F31E ; fully-qualified # 🌞 sun with face -"🪐", // 1FA90 ; fully-qualified # 🪐 ringed planet -"⭐", // 2B50 ; fully-qualified # ⭐ star -"🌟", // 1F31F ; fully-qualified # 🌟 glowing star -"🌠", // 1F320 ; fully-qualified # 🌠 shooting star -"🌌", // 1F30C ; fully-qualified # 🌌 milky way -"⛅", // 26C5 ; fully-qualified # ⛅ sun behind cloud -"🌀", // 1F300 ; fully-qualified # 🌀 cyclone -"🌈", // 1F308 ; fully-qualified # 🌈 rainbow -"🌂", // 1F302 ; fully-qualified # 🌂 closed umbrella -"☔", // 2614 ; fully-qualified # ☔ umbrella with rain drops -"⚡", // 26A1 ; fully-qualified # ⚡ high voltage -"⛄", // 26C4 ; fully-qualified # ⛄ snowman without snow -"🔥", // 1F525 ; fully-qualified # 🔥 fire -"💧", // 1F4A7 ; fully-qualified # 💧 droplet -"🌊", // 1F30A ; fully-qualified # 🌊 water wave -}, -{ // # group: Activities -"🎃", // 1F383 ; fully-qualified # 🎃 jack-o-lantern -"🎄", // 1F384 ; fully-qualified # 🎄 Christmas tree -"🎆", // 1F386 ; fully-qualified # 🎆 fireworks -"🎇", // 1F387 ; fully-qualified # 🎇 sparkler -"🧨", // 1F9E8 ; fully-qualified # 🧨 firecracker -"✨", // 2728 ; fully-qualified # ✨ sparkles -"🎈", // 1F388 ; fully-qualified # 🎈 balloon -"🎉", // 1F389 ; fully-qualified # 🎉 party popper -"🎊", // 1F38A ; fully-qualified # 🎊 confetti ball -"🎋", // 1F38B ; fully-qualified # 🎋 tanabata tree -"🎍", // 1F38D ; fully-qualified # 🎍 pine decoration -"🎎", // 1F38E ; fully-qualified # 🎎 Japanese dolls -"🎏", // 1F38F ; fully-qualified # 🎏 carp streamer -"🎐", // 1F390 ; fully-qualified # 🎐 wind chime -"🎑", // 1F391 ; fully-qualified # 🎑 moon viewing ceremony -"🧧", // 1F9E7 ; fully-qualified # 🧧 red envelope -"🎀", // 1F380 ; fully-qualified # 🎀 ribbon -"🎁", // 1F381 ; fully-qualified # 🎁 wrapped gift -"🎫", // 1F3AB ; fully-qualified # 🎫 ticket -"🏆", // 1F3C6 ; fully-qualified # 🏆 trophy -"🏅", // 1F3C5 ; fully-qualified # 🏅 sports medal -"🥇", // 1F947 ; fully-qualified # 🥇 1st place medal -"🥈", // 1F948 ; fully-qualified # 🥈 2nd place medal -"🥉", // 1F949 ; fully-qualified # 🥉 3rd place medal -"⚽", // 26BD ; fully-qualified # ⚽ soccer ball -"⚾", // 26BE ; fully-qualified # ⚾ baseball -"🥎", // 1F94E ; fully-qualified # 🥎 softball -"🏀", // 1F3C0 ; fully-qualified # 🏀 basketball -"🏐", // 1F3D0 ; fully-qualified # 🏐 volleyball -"🏈", // 1F3C8 ; fully-qualified # 🏈 american football -"🏉", // 1F3C9 ; fully-qualified # 🏉 rugby football -"🎾", // 1F3BE ; fully-qualified # 🎾 tennis -"🥏", // 1F94F ; fully-qualified # 🥏 flying disc -"🎳", // 1F3B3 ; fully-qualified # 🎳 bowling -"🏏", // 1F3CF ; fully-qualified # 🏏 cricket game -"🏑", // 1F3D1 ; fully-qualified # 🏑 field hockey -"🏒", // 1F3D2 ; fully-qualified # 🏒 ice hockey -"🥍", // 1F94D ; fully-qualified # 🥍 lacrosse -"🏓", // 1F3D3 ; fully-qualified # 🏓 ping pong -"🏸", // 1F3F8 ; fully-qualified # 🏸 badminton -"🥊", // 1F94A ; fully-qualified # 🥊 boxing glove -"🥋", // 1F94B ; fully-qualified # 🥋 martial arts uniform -"🥅", // 1F945 ; fully-qualified # 🥅 goal net -"⛳", // 26F3 ; fully-qualified # ⛳ flag in hole -"🎣", // 1F3A3 ; fully-qualified # 🎣 fishing pole -"🤿", // 1F93F ; fully-qualified # 🤿 diving mask -"🎽", // 1F3BD ; fully-qualified # 🎽 running shirt -"🎿", // 1F3BF ; fully-qualified # 🎿 skis -"🛷", // 1F6F7 ; fully-qualified # 🛷 sled -"🥌", // 1F94C ; fully-qualified # 🥌 curling stone -"🎯", // 1F3AF ; fully-qualified # 🎯 direct hit -"🪀", // 1FA80 ; fully-qualified # 🪀 yo-yo -"🪁", // 1FA81 ; fully-qualified # 🪁 kite -"🎱", // 1F3B1 ; fully-qualified # 🎱 pool 8 ball -"🔮", // 1F52E ; fully-qualified # 🔮 crystal ball -"🧿", // 1F9FF ; fully-qualified # 🧿 nazar amulet -"🎮", // 1F3AE ; fully-qualified # 🎮 video game -"🎰", // 1F3B0 ; fully-qualified # 🎰 slot machine -"🎲", // 1F3B2 ; fully-qualified # 🎲 game die -"🧩", // 1F9E9 ; fully-qualified # 🧩 puzzle piece -"🧸", // 1F9F8 ; fully-qualified # 🧸 teddy bear -"🃏", // 1F0CF ; fully-qualified # 🃏 joker -"🀄", // 1F004 ; fully-qualified # 🀄 mahjong red dragon -"🎴", // 1F3B4 ; fully-qualified # 🎴 flower playing cards -"🎭", // 1F3AD ; fully-qualified # 🎭 performing arts -"🎨", // 1F3A8 ; fully-qualified # 🎨 artist palette -"🧵", // 1F9F5 ; fully-qualified # 🧵 thread -"🧶", // 1F9F6 ; fully-qualified # 🧶 yarn -}, -{ // # group: Objects -"👓", // 1F453 ; fully-qualified # 👓 glasses -"🥽", // 1F97D ; fully-qualified # 🥽 goggles -"🥼", // 1F97C ; fully-qualified # 🥼 lab coat -"🦺", // 1F9BA ; fully-qualified # 🦺 safety vest -"👔", // 1F454 ; fully-qualified # 👔 necktie -"👕", // 1F455 ; fully-qualified # 👕 t-shirt -"👖", // 1F456 ; fully-qualified # 👖 jeans -"🧣", // 1F9E3 ; fully-qualified # 🧣 scarf -"🧤", // 1F9E4 ; fully-qualified # 🧤 gloves -"🧥", // 1F9E5 ; fully-qualified # 🧥 coat -"🧦", // 1F9E6 ; fully-qualified # 🧦 socks -"👗", // 1F457 ; fully-qualified # 👗 dress -"👘", // 1F458 ; fully-qualified # 👘 kimono -"🥻", // 1F97B ; fully-qualified # 🥻 sari -"🩱", // 1FA71 ; fully-qualified # 🩱 one-piece swimsuit -"🩲", // 1FA72 ; fully-qualified # 🩲 swim brief -"🩳", // 1FA73 ; fully-qualified # 🩳 shorts -"👙", // 1F459 ; fully-qualified # 👙 bikini -"👚", // 1F45A ; fully-qualified # 👚 woman’s clothes -"👛", // 1F45B ; fully-qualified # 👛 purse -"👜", // 1F45C ; fully-qualified # 👜 handbag -"👝", // 1F45D ; fully-qualified # 👝 clutch bag -"🎒", // 1F392 ; fully-qualified # 🎒 backpack -"👞", // 1F45E ; fully-qualified # 👞 man’s shoe -"👟", // 1F45F ; fully-qualified # 👟 running shoe -"🥾", // 1F97E ; fully-qualified # 🥾 hiking boot -"🥿", // 1F97F ; fully-qualified # 🥿 flat shoe -"👠", // 1F460 ; fully-qualified # 👠 high-heeled shoe -"👡", // 1F461 ; fully-qualified # 👡 woman’s sandal -"🩰", // 1FA70 ; fully-qualified # 🩰 ballet shoes -"👢", // 1F462 ; fully-qualified # 👢 woman’s boot -"👑", // 1F451 ; fully-qualified # 👑 crown -"👒", // 1F452 ; fully-qualified # 👒 woman’s hat -"🎩", // 1F3A9 ; fully-qualified # 🎩 top hat -"🎓", // 1F393 ; fully-qualified # 🎓 graduation cap -"🧢", // 1F9E2 ; fully-qualified # 🧢 billed cap -"📿", // 1F4FF ; fully-qualified # 📿 prayer beads -"💄", // 1F484 ; fully-qualified # 💄 lipstick -"💍", // 1F48D ; fully-qualified # 💍 ring -"💎", // 1F48E ; fully-qualified # 💎 gem stone -"🔇", // 1F507 ; fully-qualified # 🔇 muted speaker -"🔈", // 1F508 ; fully-qualified # 🔈 speaker low volume -"🔉", // 1F509 ; fully-qualified # 🔉 speaker medium volume -"🔊", // 1F50A ; fully-qualified # 🔊 speaker high volume -"📢", // 1F4E2 ; fully-qualified # 📢 loudspeaker -"📣", // 1F4E3 ; fully-qualified # 📣 megaphone -"📯", // 1F4EF ; fully-qualified # 📯 postal horn -"🔔", // 1F514 ; fully-qualified # 🔔 bell -"🔕", // 1F515 ; fully-qualified # 🔕 bell with slash -"🎼", // 1F3BC ; fully-qualified # 🎼 musical score -"🎵", // 1F3B5 ; fully-qualified # 🎵 musical note -"🎶", // 1F3B6 ; fully-qualified # 🎶 musical notes -"🎤", // 1F3A4 ; fully-qualified # 🎤 microphone -"🎧", // 1F3A7 ; fully-qualified # 🎧 headphone -"📻", // 1F4FB ; fully-qualified # 📻 radio -"🎷", // 1F3B7 ; fully-qualified # 🎷 saxophone -"🎸", // 1F3B8 ; fully-qualified # 🎸 guitar -"🎹", // 1F3B9 ; fully-qualified # 🎹 musical keyboard -"🎺", // 1F3BA ; fully-qualified # 🎺 trumpet -"🎻", // 1F3BB ; fully-qualified # 🎻 violin -"🪕", // 1FA95 ; fully-qualified # 🪕 banjo -"🥁", // 1F941 ; fully-qualified # 🥁 drum -"📱", // 1F4F1 ; fully-qualified # 📱 mobile phone -"📲", // 1F4F2 ; fully-qualified # 📲 mobile phone with arrow -"📞", // 1F4DE ; fully-qualified # 📞 telephone receiver -"📟", // 1F4DF ; fully-qualified # 📟 pager -"📠", // 1F4E0 ; fully-qualified # 📠 fax machine -"🔋", // 1F50B ; fully-qualified # 🔋 battery -"🔌", // 1F50C ; fully-qualified # 🔌 electric plug -"💻", // 1F4BB ; fully-qualified # 💻 laptop computer -"💽", // 1F4BD ; fully-qualified # 💽 computer disk -"💾", // 1F4BE ; fully-qualified # 💾 floppy disk -"💿", // 1F4BF ; fully-qualified # 💿 optical disk -"📀", // 1F4C0 ; fully-qualified # 📀 dvd -"🧮", // 1F9EE ; fully-qualified # 🧮 abacus -"🎥", // 1F3A5 ; fully-qualified # 🎥 movie camera -"🎬", // 1F3AC ; fully-qualified # 🎬 clapper board -"📺", // 1F4FA ; fully-qualified # 📺 television -"📷", // 1F4F7 ; fully-qualified # 📷 camera -"📸", // 1F4F8 ; fully-qualified # 📸 camera with flash -"📹", // 1F4F9 ; fully-qualified # 📹 video camera -"📼", // 1F4FC ; fully-qualified # 📼 videocassette -"🔍", // 1F50D ; fully-qualified # 🔍 magnifying glass tilted left -"🔎", // 1F50E ; fully-qualified # 🔎 magnifying glass tilted right -"💡", // 1F4A1 ; fully-qualified # 💡 light bulb -"🔦", // 1F526 ; fully-qualified # 🔦 flashlight -"🏮", // 1F3EE ; fully-qualified # 🏮 red paper lantern -"🪔", // 1FA94 ; fully-qualified # 🪔 diya lamp -"📔", // 1F4D4 ; fully-qualified # 📔 notebook with decorative cover -"📕", // 1F4D5 ; fully-qualified # 📕 closed book -"📖", // 1F4D6 ; fully-qualified # 📖 open book -"📗", // 1F4D7 ; fully-qualified # 📗 green book -"📘", // 1F4D8 ; fully-qualified # 📘 blue book -"📙", // 1F4D9 ; fully-qualified # 📙 orange book -"📚", // 1F4DA ; fully-qualified # 📚 books -"📓", // 1F4D3 ; fully-qualified # 📓 notebook -"📒", // 1F4D2 ; fully-qualified # 📒 ledger -"📃", // 1F4C3 ; fully-qualified # 📃 page with curl -"📜", // 1F4DC ; fully-qualified # 📜 scroll -"📄", // 1F4C4 ; fully-qualified # 📄 page facing up -"📰", // 1F4F0 ; fully-qualified # 📰 newspaper -"📑", // 1F4D1 ; fully-qualified # 📑 bookmark tabs -"🔖", // 1F516 ; fully-qualified # 🔖 bookmark -"💰", // 1F4B0 ; fully-qualified # 💰 money bag -"💴", // 1F4B4 ; fully-qualified # 💴 yen banknote -"💵", // 1F4B5 ; fully-qualified # 💵 dollar banknote -"💶", // 1F4B6 ; fully-qualified # 💶 euro banknote -"💷", // 1F4B7 ; fully-qualified # 💷 pound banknote -"💸", // 1F4B8 ; fully-qualified # 💸 money with wings -"💳", // 1F4B3 ; fully-qualified # 💳 credit card -"🧾", // 1F9FE ; fully-qualified # 🧾 receipt -"💹", // 1F4B9 ; fully-qualified # 💹 chart increasing with yen -"💱", // 1F4B1 ; fully-qualified # 💱 currency exchange -"💲", // 1F4B2 ; fully-qualified # 💲 heavy dollar sign -"📧", // 1F4E7 ; fully-qualified # 📧 e-mail -"📨", // 1F4E8 ; fully-qualified # 📨 incoming envelope -"📩", // 1F4E9 ; fully-qualified # 📩 envelope with arrow -"📤", // 1F4E4 ; fully-qualified # 📤 outbox tray -"📥", // 1F4E5 ; fully-qualified # 📥 inbox tray -"📦", // 1F4E6 ; fully-qualified # 📦 package -"📫", // 1F4EB ; fully-qualified # 📫 closed mailbox with raised flag -"📪", // 1F4EA ; fully-qualified # 📪 closed mailbox with lowered flag -"📬", // 1F4EC ; fully-qualified # 📬 open mailbox with raised flag -"📭", // 1F4ED ; fully-qualified # 📭 open mailbox with lowered flag -"📮", // 1F4EE ; fully-qualified # 📮 postbox -"📝", // 1F4DD ; fully-qualified # 📝 memo -"💼", // 1F4BC ; fully-qualified # 💼 briefcase -"📁", // 1F4C1 ; fully-qualified # 📁 file folder -"📂", // 1F4C2 ; fully-qualified # 📂 open file folder -"📅", // 1F4C5 ; fully-qualified # 📅 calendar -"📆", // 1F4C6 ; fully-qualified # 📆 tear-off calendar -"📇", // 1F4C7 ; fully-qualified # 📇 card index -"📈", // 1F4C8 ; fully-qualified # 📈 chart increasing -"📉", // 1F4C9 ; fully-qualified # 📉 chart decreasing -"📊", // 1F4CA ; fully-qualified # 📊 bar chart -"📋", // 1F4CB ; fully-qualified # 📋 clipboard -"📌", // 1F4CC ; fully-qualified # 📌 pushpin -"📍", // 1F4CD ; fully-qualified # 📍 round pushpin -"📎", // 1F4CE ; fully-qualified # 📎 paperclip -"📏", // 1F4CF ; fully-qualified # 📏 straight ruler -"📐", // 1F4D0 ; fully-qualified # 📐 triangular ruler -"🔒", // 1F512 ; fully-qualified # 🔒 locked -"🔓", // 1F513 ; fully-qualified # 🔓 unlocked -"🔏", // 1F50F ; fully-qualified # 🔏 locked with pen -"🔐", // 1F510 ; fully-qualified # 🔐 locked with key -"🔑", // 1F511 ; fully-qualified # 🔑 key -"🔨", // 1F528 ; fully-qualified # 🔨 hammer -"🪓", // 1FA93 ; fully-qualified # 🪓 axe -"🔫", // 1F52B ; fully-qualified # 🔫 pistol -"🏹", // 1F3F9 ; fully-qualified # 🏹 bow and arrow -"🔧", // 1F527 ; fully-qualified # 🔧 wrench -"🔩", // 1F529 ; fully-qualified # 🔩 nut and bolt -"🦯", // 1F9AF ; fully-qualified # 🦯 probing cane -"🔗", // 1F517 ; fully-qualified # 🔗 link -"🧰", // 1F9F0 ; fully-qualified # 🧰 toolbox -"🧲", // 1F9F2 ; fully-qualified # 🧲 magnet -"🧪", // 1F9EA ; fully-qualified # 🧪 test tube -"🧫", // 1F9EB ; fully-qualified # 🧫 petri dish -"🧬", // 1F9EC ; fully-qualified # 🧬 dna -"🔬", // 1F52C ; fully-qualified # 🔬 microscope -"🔭", // 1F52D ; fully-qualified # 🔭 telescope -"📡", // 1F4E1 ; fully-qualified # 📡 satellite antenna -"💉", // 1F489 ; fully-qualified # 💉 syringe -"🩸", // 1FA78 ; fully-qualified # 🩸 drop of blood -"💊", // 1F48A ; fully-qualified # 💊 pill -"🩹", // 1FA79 ; fully-qualified # 🩹 adhesive bandage -"🩺", // 1FA7A ; fully-qualified # 🩺 stethoscope -"🚪", // 1F6AA ; fully-qualified # 🚪 door -"🪑", // 1FA91 ; fully-qualified # 🪑 chair -"🚽", // 1F6BD ; fully-qualified # 🚽 toilet -"🚿", // 1F6BF ; fully-qualified # 🚿 shower -"🛁", // 1F6C1 ; fully-qualified # 🛁 bathtub -"🪒", // 1FA92 ; fully-qualified # 🪒 razor -"🧴", // 1F9F4 ; fully-qualified # 🧴 lotion bottle -"🧷", // 1F9F7 ; fully-qualified # 🧷 safety pin -"🧹", // 1F9F9 ; fully-qualified # 🧹 broom -"🧺", // 1F9FA ; fully-qualified # 🧺 basket -"🧻", // 1F9FB ; fully-qualified # 🧻 roll of paper -"🧼", // 1F9FC ; fully-qualified # 🧼 soap -"🧽", // 1F9FD ; fully-qualified # 🧽 sponge -"🧯", // 1F9EF ; fully-qualified # 🧯 fire extinguisher -"🛒", // 1F6D2 ; fully-qualified # 🛒 shopping cart -"🚬", // 1F6AC ; fully-qualified # 🚬 cigarette -"🗿", // 1F5FF ; fully-qualified # 🗿 moai -}, -{ // # group: Symbols -"🏧", // 1F3E7 ; fully-qualified # 🏧 ATM sign -"🚮", // 1F6AE ; fully-qualified # 🚮 litter in bin sign -"🚰", // 1F6B0 ; fully-qualified # 🚰 potable water -"♿", // 267F ; fully-qualified # ♿ wheelchair symbol -"🚹", // 1F6B9 ; fully-qualified # 🚹 men’s room -"🚺", // 1F6BA ; fully-qualified # 🚺 women’s room -"🚻", // 1F6BB ; fully-qualified # 🚻 restroom -"🚼", // 1F6BC ; fully-qualified # 🚼 baby symbol -"🚾", // 1F6BE ; fully-qualified # 🚾 water closet -"🛂", // 1F6C2 ; fully-qualified # 🛂 passport control -"🛃", // 1F6C3 ; fully-qualified # 🛃 customs -"🛄", // 1F6C4 ; fully-qualified # 🛄 baggage claim -"🛅", // 1F6C5 ; fully-qualified # 🛅 left luggage -"🚸", // 1F6B8 ; fully-qualified # 🚸 children crossing -"⛔", // 26D4 ; fully-qualified # ⛔ no entry -"🚫", // 1F6AB ; fully-qualified # 🚫 prohibited -"🚳", // 1F6B3 ; fully-qualified # 🚳 no bicycles -"🚭", // 1F6AD ; fully-qualified # 🚭 no smoking -"🚯", // 1F6AF ; fully-qualified # 🚯 no littering -"🚱", // 1F6B1 ; fully-qualified # 🚱 non-potable water -"🚷", // 1F6B7 ; fully-qualified # 🚷 no pedestrians -"📵", // 1F4F5 ; fully-qualified # 📵 no mobile phones -"🔞", // 1F51E ; fully-qualified # 🔞 no one under eighteen -"🔃", // 1F503 ; fully-qualified # 🔃 clockwise vertical arrows -"🔄", // 1F504 ; fully-qualified # 🔄 counterclockwise arrows button -"🔙", // 1F519 ; fully-qualified # 🔙 BACK arrow -"🔚", // 1F51A ; fully-qualified # 🔚 END arrow -"🔛", // 1F51B ; fully-qualified # 🔛 ON! arrow -"🔜", // 1F51C ; fully-qualified # 🔜 SOON arrow -"🔝", // 1F51D ; fully-qualified # 🔝 TOP arrow -"🛐", // 1F6D0 ; fully-qualified # 🛐 place of worship -"🕎", // 1F54E ; fully-qualified # 🕎 menorah -"🔯", // 1F52F ; fully-qualified # 🔯 dotted six-pointed star -"♈", // 2648 ; fully-qualified # ♈ Aries -"♉", // 2649 ; fully-qualified # ♉ Taurus -"♊", // 264A ; fully-qualified # ♊ Gemini -"♋", // 264B ; fully-qualified # ♋ Cancer -"♌", // 264C ; fully-qualified # ♌ Leo -"♍", // 264D ; fully-qualified # ♍ Virgo -"♎", // 264E ; fully-qualified # ♎ Libra -"♏", // 264F ; fully-qualified # ♏ Scorpio -"♐", // 2650 ; fully-qualified # ♐ Sagittarius -"♑", // 2651 ; fully-qualified # ♑ Capricorn -"♒", // 2652 ; fully-qualified # ♒ Aquarius -"♓", // 2653 ; fully-qualified # ♓ Pisces -"⛎", // 26CE ; fully-qualified # ⛎ Ophiuchus -"🔀", // 1F500 ; fully-qualified # 🔀 shuffle tracks button -"🔁", // 1F501 ; fully-qualified # 🔁 repeat button -"🔂", // 1F502 ; fully-qualified # 🔂 repeat single button -"⏩", // 23E9 ; fully-qualified # ⏩ fast-forward button -"⏪", // 23EA ; fully-qualified # ⏪ fast reverse button -"🔼", // 1F53C ; fully-qualified # 🔼 upwards button -"⏫", // 23EB ; fully-qualified # ⏫ fast up button -"🔽", // 1F53D ; fully-qualified # 🔽 downwards button -"⏬", // 23EC ; fully-qualified # ⏬ fast down button -"🎦", // 1F3A6 ; fully-qualified # 🎦 cinema -"🔅", // 1F505 ; fully-qualified # 🔅 dim button -"🔆", // 1F506 ; fully-qualified # 🔆 bright button -"📶", // 1F4F6 ; fully-qualified # 📶 antenna bars -"📳", // 1F4F3 ; fully-qualified # 📳 vibration mode -"📴", // 1F4F4 ; fully-qualified # 📴 mobile phone off -"🔱", // 1F531 ; fully-qualified # 🔱 trident emblem -"📛", // 1F4DB ; fully-qualified # 📛 name badge -"🔰", // 1F530 ; fully-qualified # 🔰 Japanese symbol for beginner -"⭕", // 2B55 ; fully-qualified # ⭕ hollow red circle -"✅", // 2705 ; fully-qualified # ✅ check mark button -"❌", // 274C ; fully-qualified # ❌ cross mark -"❎", // 274E ; fully-qualified # ❎ cross mark button -"➕", // 2795 ; fully-qualified # ➕ plus sign -"➖", // 2796 ; fully-qualified # ➖ minus sign -"➗", // 2797 ; fully-qualified # ➗ division sign -"➰", // 27B0 ; fully-qualified # ➰ curly loop -"➿", // 27BF ; fully-qualified # ➿ double curly loop -"❓", // 2753 ; fully-qualified # ❓ question mark -"❔", // 2754 ; fully-qualified # ❔ white question mark -"❕", // 2755 ; fully-qualified # ❕ white exclamation mark -"❗", // 2757 ; fully-qualified # ❗ exclamation mark -"🔟", // 1F51F ; fully-qualified # 🔟 keycap: 10 -"🔠", // 1F520 ; fully-qualified # 🔠 input latin uppercase -"🔡", // 1F521 ; fully-qualified # 🔡 input latin lowercase -"🔢", // 1F522 ; fully-qualified # 🔢 input numbers -"🔣", // 1F523 ; fully-qualified # 🔣 input symbols -"🔤", // 1F524 ; fully-qualified # 🔤 input latin letters -"🆎", // 1F18E ; fully-qualified # 🆎 AB button (blood type) -"🆑", // 1F191 ; fully-qualified # 🆑 CL button -"🆒", // 1F192 ; fully-qualified # 🆒 COOL button -"🆓", // 1F193 ; fully-qualified # 🆓 FREE button -"🆔", // 1F194 ; fully-qualified # 🆔 ID button -"🆕", // 1F195 ; fully-qualified # 🆕 NEW button -"🆖", // 1F196 ; fully-qualified # 🆖 NG button -"🆗", // 1F197 ; fully-qualified # 🆗 OK button -"🆘", // 1F198 ; fully-qualified # 🆘 SOS button -"🆙", // 1F199 ; fully-qualified # 🆙 UP! button -"🆚", // 1F19A ; fully-qualified # 🆚 VS button -"🈁", // 1F201 ; fully-qualified # 🈁 Japanese “here” button -"🈶", // 1F236 ; fully-qualified # 🈶 Japanese “not free of charge” button -"🈯", // 1F22F ; fully-qualified # 🈯 Japanese “reserved” button -"🉐", // 1F250 ; fully-qualified # 🉐 Japanese “bargain” button -"🈹", // 1F239 ; fully-qualified # 🈹 Japanese “discount” button -"🈚", // 1F21A ; fully-qualified # 🈚 Japanese “free of charge” button -"🈲", // 1F232 ; fully-qualified # 🈲 Japanese “prohibited” button -"🉑", // 1F251 ; fully-qualified # 🉑 Japanese “acceptable” button -"🈸", // 1F238 ; fully-qualified # 🈸 Japanese “application” button -"🈴", // 1F234 ; fully-qualified # 🈴 Japanese “passing grade” button -"🈳", // 1F233 ; fully-qualified # 🈳 Japanese “vacancy” button -"🈺", // 1F23A ; fully-qualified # 🈺 Japanese “open for business” button -"🈵", // 1F235 ; fully-qualified # 🈵 Japanese “no vacancy” button -"🔴", // 1F534 ; fully-qualified # 🔴 red circle -"🟠", // 1F7E0 ; fully-qualified # 🟠 orange circle -"🟡", // 1F7E1 ; fully-qualified # 🟡 yellow circle -"🟢", // 1F7E2 ; fully-qualified # 🟢 green circle -"🔵", // 1F535 ; fully-qualified # 🔵 blue circle -"🟣", // 1F7E3 ; fully-qualified # 🟣 purple circle -"🟤", // 1F7E4 ; fully-qualified # 🟤 brown circle -"⚫", // 26AB ; fully-qualified # ⚫ black circle -"⚪", // 26AA ; fully-qualified # ⚪ white circle -"🟥", // 1F7E5 ; fully-qualified # 🟥 red square -"🟧", // 1F7E7 ; fully-qualified # 🟧 orange square -"🟨", // 1F7E8 ; fully-qualified # 🟨 yellow square -"🟩", // 1F7E9 ; fully-qualified # 🟩 green square -"🟦", // 1F7E6 ; fully-qualified # 🟦 blue square -"🟪", // 1F7EA ; fully-qualified # 🟪 purple square -"🟫", // 1F7EB ; fully-qualified # 🟫 brown square -"⬛", // 2B1B ; fully-qualified # ⬛ black large square -"⬜", // 2B1C ; fully-qualified # ⬜ white large square -"◾", // 25FE ; fully-qualified # ◾ black medium-small square -"◽", // 25FD ; fully-qualified # ◽ white medium-small square -"🔶", // 1F536 ; fully-qualified # 🔶 large orange diamond -"🔷", // 1F537 ; fully-qualified # 🔷 large blue diamond -"🔸", // 1F538 ; fully-qualified # 🔸 small orange diamond -"🔹", // 1F539 ; fully-qualified # 🔹 small blue diamond -"🔺", // 1F53A ; fully-qualified # 🔺 red triangle pointed up -"🔻", // 1F53B ; fully-qualified # 🔻 red triangle pointed down -"💠", // 1F4A0 ; fully-qualified # 💠 diamond with a dot -"🔘", // 1F518 ; fully-qualified # 🔘 radio button -"🔳", // 1F533 ; fully-qualified # 🔳 white square button -"🔲", // 1F532 ; fully-qualified # 🔲 black square button -}, -{ // # group: Flags -"🏁", // 1F3C1 ; fully-qualified # 🏁 chequered flag -"🚩", // 1F6A9 ; fully-qualified # 🚩 triangular flag -"🎌", // 1F38C ; fully-qualified # 🎌 crossed flags -"🏴", // 1F3F4 ; fully-qualified # 🏴 black flag -} - }; +public class Emojis { + public static final String[][] EMOJIS = + new String[][] { + { // # group: Smileys & Emotion + "😀", // 1F600 ; fully-qualified # 😀 grinning face + "😃", // 1F603 ; fully-qualified # 😃 grinning face with big eyes + "😄", // 1F604 ; fully-qualified # 😄 grinning face with smiling eyes + "😁", // 1F601 ; fully-qualified # 😁 beaming face with smiling eyes + "😆", // 1F606 ; fully-qualified # 😆 grinning squinting face + "😅", // 1F605 ; fully-qualified # 😅 grinning face with sweat + "🤣", // 1F923 ; fully-qualified # 🤣 rolling on the floor laughing + "😂", // 1F602 ; fully-qualified # 😂 face with tears of joy + "🙂", // 1F642 ; fully-qualified # 🙂 slightly smiling face + "🙃", // 1F643 ; fully-qualified # 🙃 upside-down face + "😉", // 1F609 ; fully-qualified # 😉 winking face + "😊", // 1F60A ; fully-qualified # 😊 smiling face with smiling eyes + "😇", // 1F607 ; fully-qualified # 😇 smiling face with halo + "🥰", // 1F970 ; fully-qualified # 🥰 smiling face with hearts + "😍", // 1F60D ; fully-qualified # 😍 smiling face with heart-eyes + "🤩", // 1F929 ; fully-qualified # 🤩 star-struck + "😘", // 1F618 ; fully-qualified # 😘 face blowing a kiss + "😗", // 1F617 ; fully-qualified # 😗 kissing face + "😚", // 1F61A ; fully-qualified # 😚 kissing face with closed eyes + "😙", // 1F619 ; fully-qualified # 😙 kissing face with smiling eyes + "😋", // 1F60B ; fully-qualified # 😋 face savoring food + "😛", // 1F61B ; fully-qualified # 😛 face with tongue + "😜", // 1F61C ; fully-qualified # 😜 winking face with tongue + "🤪", // 1F92A ; fully-qualified # 🤪 zany face + "😝", // 1F61D ; fully-qualified # 😝 squinting face with tongue + "🤑", // 1F911 ; fully-qualified # 🤑 money-mouth face + "🤗", // 1F917 ; fully-qualified # 🤗 hugging face + "🤭", // 1F92D ; fully-qualified # 🤭 face with hand over mouth + "🤫", // 1F92B ; fully-qualified # 🤫 shushing face + "🤔", // 1F914 ; fully-qualified # 🤔 thinking face + "🤐", // 1F910 ; fully-qualified # 🤐 zipper-mouth face + "🤨", // 1F928 ; fully-qualified # 🤨 face with raised eyebrow + "😐", // 1F610 ; fully-qualified # 😐 neutral face + "😑", // 1F611 ; fully-qualified # 😑 expressionless face + "😶", // 1F636 ; fully-qualified # 😶 face without mouth + "😏", // 1F60F ; fully-qualified # 😏 smirking face + "😒", // 1F612 ; fully-qualified # 😒 unamused face + "🙄", // 1F644 ; fully-qualified # 🙄 face with rolling eyes + "😬", // 1F62C ; fully-qualified # 😬 grimacing face + "🤥", // 1F925 ; fully-qualified # 🤥 lying face + "😌", // 1F60C ; fully-qualified # 😌 relieved face + "😔", // 1F614 ; fully-qualified # 😔 pensive face + "😪", // 1F62A ; fully-qualified # 😪 sleepy face + "🤤", // 1F924 ; fully-qualified # 🤤 drooling face + "😴", // 1F634 ; fully-qualified # 😴 sleeping face + "😷", // 1F637 ; fully-qualified # 😷 face with medical mask + "🤒", // 1F912 ; fully-qualified # 🤒 face with thermometer + "🤕", // 1F915 ; fully-qualified # 🤕 face with head-bandage + "🤢", // 1F922 ; fully-qualified # 🤢 nauseated face + "🤮", // 1F92E ; fully-qualified # 🤮 face vomiting + "🤧", // 1F927 ; fully-qualified # 🤧 sneezing face + "🥵", // 1F975 ; fully-qualified # 🥵 hot face + "🥶", // 1F976 ; fully-qualified # 🥶 cold face + "🥴", // 1F974 ; fully-qualified # 🥴 woozy face + "😵", // 1F635 ; fully-qualified # 😵 dizzy face + "🤯", // 1F92F ; fully-qualified # 🤯 exploding head + "🤠", // 1F920 ; fully-qualified # 🤠 cowboy hat face + "🥳", // 1F973 ; fully-qualified # 🥳 partying face + "😎", // 1F60E ; fully-qualified # 😎 smiling face with sunglasses + "🤓", // 1F913 ; fully-qualified # 🤓 nerd face + "🧐", // 1F9D0 ; fully-qualified # 🧐 face with monocle + "😕", // 1F615 ; fully-qualified # 😕 confused face + "😟", // 1F61F ; fully-qualified # 😟 worried face + "🙁", // 1F641 ; fully-qualified # 🙁 slightly frowning face + "😮", // 1F62E ; fully-qualified # 😮 face with open mouth + "😯", // 1F62F ; fully-qualified # 😯 hushed face + "😲", // 1F632 ; fully-qualified # 😲 astonished face + "😳", // 1F633 ; fully-qualified # 😳 flushed face + "🥺", // 1F97A ; fully-qualified # 🥺 pleading face + "😦", // 1F626 ; fully-qualified # 😦 frowning face with open mouth + "😧", // 1F627 ; fully-qualified # 😧 anguished face + "😨", // 1F628 ; fully-qualified # 😨 fearful face + "😰", // 1F630 ; fully-qualified # 😰 anxious face with sweat + "😥", // 1F625 ; fully-qualified # 😥 sad but relieved face + "😢", // 1F622 ; fully-qualified # 😢 crying face + "😭", // 1F62D ; fully-qualified # 😭 loudly crying face + "😱", // 1F631 ; fully-qualified # 😱 face screaming in fear + "😖", // 1F616 ; fully-qualified # 😖 confounded face + "😣", // 1F623 ; fully-qualified # 😣 persevering face + "😞", // 1F61E ; fully-qualified # 😞 disappointed face + "😓", // 1F613 ; fully-qualified # 😓 downcast face with sweat + "😩", // 1F629 ; fully-qualified # 😩 weary face + "😫", // 1F62B ; fully-qualified # 😫 tired face + "🥱", // 1F971 ; fully-qualified # 🥱 yawning face + "😤", // 1F624 ; fully-qualified # 😤 face with steam from nose + "😡", // 1F621 ; fully-qualified # 😡 pouting face + "😠", // 1F620 ; fully-qualified # 😠 angry face + "🤬", // 1F92C ; fully-qualified # 🤬 face with symbols on mouth + "😈", // 1F608 ; fully-qualified # 😈 smiling face with horns + "👿", // 1F47F ; fully-qualified # 👿 angry face with horns + "💀", // 1F480 ; fully-qualified # 💀 skull + "💩", // 1F4A9 ; fully-qualified # 💩 pile of poo + "🤡", // 1F921 ; fully-qualified # 🤡 clown face + "👹", // 1F479 ; fully-qualified # 👹 ogre + "👺", // 1F47A ; fully-qualified # 👺 goblin + "👻", // 1F47B ; fully-qualified # 👻 ghost + "👽", // 1F47D ; fully-qualified # 👽 alien + "👾", // 1F47E ; fully-qualified # 👾 alien monster + "🤖", // 1F916 ; fully-qualified # 🤖 robot + "😺", // 1F63A ; fully-qualified # 😺 grinning cat + "😸", // 1F638 ; fully-qualified # 😸 grinning cat with smiling eyes + "😹", // 1F639 ; fully-qualified # 😹 cat with tears of joy + "😻", // 1F63B ; fully-qualified # 😻 smiling cat with heart-eyes + "😼", // 1F63C ; fully-qualified # 😼 cat with wry smile + "😽", // 1F63D ; fully-qualified # 😽 kissing cat + "🙀", // 1F640 ; fully-qualified # 🙀 weary cat + "😿", // 1F63F ; fully-qualified # 😿 crying cat + "😾", // 1F63E ; fully-qualified # 😾 pouting cat + "🙈", // 1F648 ; fully-qualified # 🙈 see-no-evil monkey + "🙉", // 1F649 ; fully-qualified # 🙉 hear-no-evil monkey + "🙊", // 1F64A ; fully-qualified # 🙊 speak-no-evil monkey + "💋", // 1F48B ; fully-qualified # 💋 kiss mark + "💌", // 1F48C ; fully-qualified # 💌 love letter + "💘", // 1F498 ; fully-qualified # 💘 heart with arrow + "💝", // 1F49D ; fully-qualified # 💝 heart with ribbon + "💖", // 1F496 ; fully-qualified # 💖 sparkling heart + "💗", // 1F497 ; fully-qualified # 💗 growing heart + "💓", // 1F493 ; fully-qualified # 💓 beating heart + "💞", // 1F49E ; fully-qualified # 💞 revolving hearts + "💕", // 1F495 ; fully-qualified # 💕 two hearts + "💟", // 1F49F ; fully-qualified # 💟 heart decoration + "💔", // 1F494 ; fully-qualified # 💔 broken heart + "🧡", // 1F9E1 ; fully-qualified # 🧡 orange heart + "💛", // 1F49B ; fully-qualified # 💛 yellow heart + "💚", // 1F49A ; fully-qualified # 💚 green heart + "💙", // 1F499 ; fully-qualified # 💙 blue heart + "💜", // 1F49C ; fully-qualified # 💜 purple heart + "🤎", // 1F90E ; fully-qualified # 🤎 brown heart + "🖤", // 1F5A4 ; fully-qualified # 🖤 black heart + "🤍", // 1F90D ; fully-qualified # 🤍 white heart + "💯", // 1F4AF ; fully-qualified # 💯 hundred points + "💢", // 1F4A2 ; fully-qualified # 💢 anger symbol + "💥", // 1F4A5 ; fully-qualified # 💥 collision + "💫", // 1F4AB ; fully-qualified # 💫 dizzy + "💦", // 1F4A6 ; fully-qualified # 💦 sweat droplets + "💨", // 1F4A8 ; fully-qualified # 💨 dashing away + "💣", // 1F4A3 ; fully-qualified # 💣 bomb + "💬", // 1F4AC ; fully-qualified # 💬 speech balloon + "💭", // 1F4AD ; fully-qualified # 💭 thought balloon + "💤", // 1F4A4 ; fully-qualified # 💤 zzz + }, + { // # group: People & Body + "👋", // 1F44B ; fully-qualified # 👋 waving hand + "🤚", // 1F91A ; fully-qualified # 🤚 raised back of hand + "✋", // 270B ; fully-qualified # ✋ raised hand + "🖖", // 1F596 ; fully-qualified # 🖖 vulcan salute + "👌", // 1F44C ; fully-qualified # 👌 OK hand + "🤏", // 1F90F ; fully-qualified # 🤏 pinching hand + "🤞", // 1F91E ; fully-qualified # 🤞 crossed fingers + "🤟", // 1F91F ; fully-qualified # 🤟 love-you gesture + "🤘", // 1F918 ; fully-qualified # 🤘 sign of the horns + "🤙", // 1F919 ; fully-qualified # 🤙 call me hand + "👈", // 1F448 ; fully-qualified # 👈 backhand index pointing left + "👉", // 1F449 ; fully-qualified # 👉 backhand index pointing right + "👆", // 1F446 ; fully-qualified # 👆 backhand index pointing up + "🖕", // 1F595 ; fully-qualified # 🖕 middle finger + "👇", // 1F447 ; fully-qualified # 👇 backhand index pointing down + "👍", // 1F44D ; fully-qualified # 👍 thumbs up + "👎", // 1F44E ; fully-qualified # 👎 thumbs down + "✊", // 270A ; fully-qualified # ✊ raised fist + "👊", // 1F44A ; fully-qualified # 👊 oncoming fist + "🤛", // 1F91B ; fully-qualified # 🤛 left-facing fist + "🤜", // 1F91C ; fully-qualified # 🤜 right-facing fist + "👏", // 1F44F ; fully-qualified # 👏 clapping hands + "🙌", // 1F64C ; fully-qualified # 🙌 raising hands + "👐", // 1F450 ; fully-qualified # 👐 open hands + "🤲", // 1F932 ; fully-qualified # 🤲 palms up together + "🤝", // 1F91D ; fully-qualified # 🤝 handshake + "🙏", // 1F64F ; fully-qualified # 🙏 folded hands + "💅", // 1F485 ; fully-qualified # 💅 nail polish + "🤳", // 1F933 ; fully-qualified # 🤳 selfie + "💪", // 1F4AA ; fully-qualified # 💪 flexed biceps + "🦾", // 1F9BE ; fully-qualified # 🦾 mechanical arm + "🦿", // 1F9BF ; fully-qualified # 🦿 mechanical leg + "🦵", // 1F9B5 ; fully-qualified # 🦵 leg + "🦶", // 1F9B6 ; fully-qualified # 🦶 foot + "👂", // 1F442 ; fully-qualified # 👂 ear + "🦻", // 1F9BB ; fully-qualified # 🦻 ear with hearing aid + "👃", // 1F443 ; fully-qualified # 👃 nose + "🧠", // 1F9E0 ; fully-qualified # 🧠 brain + "🦷", // 1F9B7 ; fully-qualified # 🦷 tooth + "🦴", // 1F9B4 ; fully-qualified # 🦴 bone + "👀", // 1F440 ; fully-qualified # 👀 eyes + "👅", // 1F445 ; fully-qualified # 👅 tongue + "👄", // 1F444 ; fully-qualified # 👄 mouth + "👶", // 1F476 ; fully-qualified # 👶 baby + "🧒", // 1F9D2 ; fully-qualified # 🧒 child + "👦", // 1F466 ; fully-qualified # 👦 boy + "👧", // 1F467 ; fully-qualified # 👧 girl + "🧑", // 1F9D1 ; fully-qualified # 🧑 person + "👱", // 1F471 ; fully-qualified # 👱 person: blond hair + "👨", // 1F468 ; fully-qualified # 👨 man + "🧔", // 1F9D4 ; fully-qualified # 🧔 man: beard + "👩", // 1F469 ; fully-qualified # 👩 woman + "🧓", // 1F9D3 ; fully-qualified # 🧓 older person + "👴", // 1F474 ; fully-qualified # 👴 old man + "👵", // 1F475 ; fully-qualified # 👵 old woman + "🙍", // 1F64D ; fully-qualified # 🙍 person frowning + "🙎", // 1F64E ; fully-qualified # 🙎 person pouting + "🙅", // 1F645 ; fully-qualified # 🙅 person gesturing NO + "🙆", // 1F646 ; fully-qualified # 🙆 person gesturing OK + "💁", // 1F481 ; fully-qualified # 💁 person tipping hand + "🙋", // 1F64B ; fully-qualified # 🙋 person raising hand + "🧏", // 1F9CF ; fully-qualified # 🧏 deaf person + "🙇", // 1F647 ; fully-qualified # 🙇 person bowing + "🤦", // 1F926 ; fully-qualified # 🤦 person facepalming + "🤷", // 1F937 ; fully-qualified # 🤷 person shrugging + "👮", // 1F46E ; fully-qualified # 👮 police officer + "💂", // 1F482 ; fully-qualified # 💂 guard + "👷", // 1F477 ; fully-qualified # 👷 construction worker + "🤴", // 1F934 ; fully-qualified # 🤴 prince + "👸", // 1F478 ; fully-qualified # 👸 princess + "👳", // 1F473 ; fully-qualified # 👳 person wearing turban + "👲", // 1F472 ; fully-qualified # 👲 man with Chinese cap + "🧕", // 1F9D5 ; fully-qualified # 🧕 woman with headscarf + "🤵", // 1F935 ; fully-qualified # 🤵 man in tuxedo + "👰", // 1F470 ; fully-qualified # 👰 bride with veil + "🤰", // 1F930 ; fully-qualified # 🤰 pregnant woman + "🤱", // 1F931 ; fully-qualified # 🤱 breast-feeding + "👼", // 1F47C ; fully-qualified # 👼 baby angel + "🎅", // 1F385 ; fully-qualified # 🎅 Santa Claus + "🤶", // 1F936 ; fully-qualified # 🤶 Mrs. Claus + "🦸", // 1F9B8 ; fully-qualified # 🦸 superhero + "🦹", // 1F9B9 ; fully-qualified # 🦹 supervillain + "🧙", // 1F9D9 ; fully-qualified # 🧙 mage + "🧚", // 1F9DA ; fully-qualified # 🧚 fairy + "🧛", // 1F9DB ; fully-qualified # 🧛 vampire + "🧜", // 1F9DC ; fully-qualified # 🧜 merperson + "🧝", // 1F9DD ; fully-qualified # 🧝 elf + "🧞", // 1F9DE ; fully-qualified # 🧞 genie + "🧟", // 1F9DF ; fully-qualified # 🧟 zombie + "💆", // 1F486 ; fully-qualified # 💆 person getting massage + "💇", // 1F487 ; fully-qualified # 💇 person getting haircut + "🚶", // 1F6B6 ; fully-qualified # 🚶 person walking + "🧍", // 1F9CD ; fully-qualified # 🧍 person standing + "🧎", // 1F9CE ; fully-qualified # 🧎 person kneeling + "🏃", // 1F3C3 ; fully-qualified # 🏃 person running + "💃", // 1F483 ; fully-qualified # 💃 woman dancing + "🕺", // 1F57A ; fully-qualified # 🕺 man dancing + "👯", // 1F46F ; fully-qualified # 👯 people with bunny ears + "🧖", // 1F9D6 ; fully-qualified # 🧖 person in steamy room + "🧗", // 1F9D7 ; fully-qualified # 🧗 person climbing + "🤺", // 1F93A ; fully-qualified # 🤺 person fencing + "🏇", // 1F3C7 ; fully-qualified # 🏇 horse racing + "🏂", // 1F3C2 ; fully-qualified # 🏂 snowboarder + "🏄", // 1F3C4 ; fully-qualified # 🏄 person surfing + "🚣", // 1F6A3 ; fully-qualified # 🚣 person rowing boat + "🏊", // 1F3CA ; fully-qualified # 🏊 person swimming + "🚴", // 1F6B4 ; fully-qualified # 🚴 person biking + "🚵", // 1F6B5 ; fully-qualified # 🚵 person mountain biking + "🤸", // 1F938 ; fully-qualified # 🤸 person cartwheeling + "🤼", // 1F93C ; fully-qualified # 🤼 people wrestling + "🤽", // 1F93D ; fully-qualified # 🤽 person playing water polo + "🤾", // 1F93E ; fully-qualified # 🤾 person playing handball + "🤹", // 1F939 ; fully-qualified # 🤹 person juggling + "🧘", // 1F9D8 ; fully-qualified # 🧘 person in lotus position + "🛀", // 1F6C0 ; fully-qualified # 🛀 person taking bath + "🛌", // 1F6CC ; fully-qualified # 🛌 person in bed + "👭", // 1F46D ; fully-qualified # 👭 women holding hands + "👫", // 1F46B ; fully-qualified # 👫 woman and man holding hands + "👬", // 1F46C ; fully-qualified # 👬 men holding hands + "💏", // 1F48F ; fully-qualified # 💏 kiss + "💑", // 1F491 ; fully-qualified # 💑 couple with heart + "👪", // 1F46A ; fully-qualified # 👪 family + "👤", // 1F464 ; fully-qualified # 👤 bust in silhouette + "👥", // 1F465 ; fully-qualified # 👥 busts in silhouette + "👣", // 1F463 ; fully-qualified # 👣 footprints + }, + { // # group: Animals & Nature + "🐵", // 1F435 ; fully-qualified # 🐵 monkey face + "🐒", // 1F412 ; fully-qualified # 🐒 monkey + "🦍", // 1F98D ; fully-qualified # 🦍 gorilla + "🦧", // 1F9A7 ; fully-qualified # 🦧 orangutan + "🐶", // 1F436 ; fully-qualified # 🐶 dog face + "🐕", // 1F415 ; fully-qualified # 🐕 dog + "🦮", // 1F9AE ; fully-qualified # 🦮 guide dog + "🐩", // 1F429 ; fully-qualified # 🐩 poodle + "🐺", // 1F43A ; fully-qualified # 🐺 wolf + "🦊", // 1F98A ; fully-qualified # 🦊 fox + "🦝", // 1F99D ; fully-qualified # 🦝 raccoon + "🐱", // 1F431 ; fully-qualified # 🐱 cat face + "🐈", // 1F408 ; fully-qualified # 🐈 cat + "🦁", // 1F981 ; fully-qualified # 🦁 lion + "🐯", // 1F42F ; fully-qualified # 🐯 tiger face + "🐅", // 1F405 ; fully-qualified # 🐅 tiger + "🐆", // 1F406 ; fully-qualified # 🐆 leopard + "🐴", // 1F434 ; fully-qualified # 🐴 horse face + "🐎", // 1F40E ; fully-qualified # 🐎 horse + "🦄", // 1F984 ; fully-qualified # 🦄 unicorn + "🦓", // 1F993 ; fully-qualified # 🦓 zebra + "🦌", // 1F98C ; fully-qualified # 🦌 deer + "🐮", // 1F42E ; fully-qualified # 🐮 cow face + "🐂", // 1F402 ; fully-qualified # 🐂 ox + "🐃", // 1F403 ; fully-qualified # 🐃 water buffalo + "🐄", // 1F404 ; fully-qualified # 🐄 cow + "🐷", // 1F437 ; fully-qualified # 🐷 pig face + "🐖", // 1F416 ; fully-qualified # 🐖 pig + "🐗", // 1F417 ; fully-qualified # 🐗 boar + "🐽", // 1F43D ; fully-qualified # 🐽 pig nose + "🐏", // 1F40F ; fully-qualified # 🐏 ram + "🐑", // 1F411 ; fully-qualified # 🐑 ewe + "🐐", // 1F410 ; fully-qualified # 🐐 goat + "🐪", // 1F42A ; fully-qualified # 🐪 camel + "🐫", // 1F42B ; fully-qualified # 🐫 two-hump camel + "🦙", // 1F999 ; fully-qualified # 🦙 llama + "🦒", // 1F992 ; fully-qualified # 🦒 giraffe + "🐘", // 1F418 ; fully-qualified # 🐘 elephant + "🦏", // 1F98F ; fully-qualified # 🦏 rhinoceros + "🦛", // 1F99B ; fully-qualified # 🦛 hippopotamus + "🐭", // 1F42D ; fully-qualified # 🐭 mouse face + "🐁", // 1F401 ; fully-qualified # 🐁 mouse + "🐀", // 1F400 ; fully-qualified # 🐀 rat + "🐹", // 1F439 ; fully-qualified # 🐹 hamster + "🐰", // 1F430 ; fully-qualified # 🐰 rabbit face + "🐇", // 1F407 ; fully-qualified # 🐇 rabbit + "🦔", // 1F994 ; fully-qualified # 🦔 hedgehog + "🦇", // 1F987 ; fully-qualified # 🦇 bat + "🐻", // 1F43B ; fully-qualified # 🐻 bear + "🐨", // 1F428 ; fully-qualified # 🐨 koala + "🐼", // 1F43C ; fully-qualified # 🐼 panda + "🦥", // 1F9A5 ; fully-qualified # 🦥 sloth + "🦦", // 1F9A6 ; fully-qualified # 🦦 otter + "🦨", // 1F9A8 ; fully-qualified # 🦨 skunk + "🦘", // 1F998 ; fully-qualified # 🦘 kangaroo + "🦡", // 1F9A1 ; fully-qualified # 🦡 badger + "🐾", // 1F43E ; fully-qualified # 🐾 paw prints + "🦃", // 1F983 ; fully-qualified # 🦃 turkey + "🐔", // 1F414 ; fully-qualified # 🐔 chicken + "🐓", // 1F413 ; fully-qualified # 🐓 rooster + "🐣", // 1F423 ; fully-qualified # 🐣 hatching chick + "🐤", // 1F424 ; fully-qualified # 🐤 baby chick + "🐥", // 1F425 ; fully-qualified # 🐥 front-facing baby chick + "🐦", // 1F426 ; fully-qualified # 🐦 bird + "🐧", // 1F427 ; fully-qualified # 🐧 penguin + "🦅", // 1F985 ; fully-qualified # 🦅 eagle + "🦆", // 1F986 ; fully-qualified # 🦆 duck + "🦢", // 1F9A2 ; fully-qualified # 🦢 swan + "🦉", // 1F989 ; fully-qualified # 🦉 owl + "🦩", // 1F9A9 ; fully-qualified # 🦩 flamingo + "🦚", // 1F99A ; fully-qualified # 🦚 peacock + "🦜", // 1F99C ; fully-qualified # 🦜 parrot + "🐸", // 1F438 ; fully-qualified # 🐸 frog + "🐊", // 1F40A ; fully-qualified # 🐊 crocodile + "🐢", // 1F422 ; fully-qualified # 🐢 turtle + "🦎", // 1F98E ; fully-qualified # 🦎 lizard + "🐍", // 1F40D ; fully-qualified # 🐍 snake + "🐲", // 1F432 ; fully-qualified # 🐲 dragon face + "🐉", // 1F409 ; fully-qualified # 🐉 dragon + "🦕", // 1F995 ; fully-qualified # 🦕 sauropod + "🦖", // 1F996 ; fully-qualified # 🦖 T-Rex + "🐳", // 1F433 ; fully-qualified # 🐳 spouting whale + "🐋", // 1F40B ; fully-qualified # 🐋 whale + "🐬", // 1F42C ; fully-qualified # 🐬 dolphin + "🐟", // 1F41F ; fully-qualified # 🐟 fish + "🐠", // 1F420 ; fully-qualified # 🐠 tropical fish + "🐡", // 1F421 ; fully-qualified # 🐡 blowfish + "🦈", // 1F988 ; fully-qualified # 🦈 shark + "🐙", // 1F419 ; fully-qualified # 🐙 octopus + "🐚", // 1F41A ; fully-qualified # 🐚 spiral shell + "🐌", // 1F40C ; fully-qualified # 🐌 snail + "🦋", // 1F98B ; fully-qualified # 🦋 butterfly + "🐛", // 1F41B ; fully-qualified # 🐛 bug + "🐜", // 1F41C ; fully-qualified # 🐜 ant + "🐝", // 1F41D ; fully-qualified # 🐝 honeybee + "🐞", // 1F41E ; fully-qualified # 🐞 lady beetle + "🦗", // 1F997 ; fully-qualified # 🦗 cricket + "🦂", // 1F982 ; fully-qualified # 🦂 scorpion + "🦟", // 1F99F ; fully-qualified # 🦟 mosquito + "🦠", // 1F9A0 ; fully-qualified # 🦠 microbe + "💐", // 1F490 ; fully-qualified # 💐 bouquet + "🌸", // 1F338 ; fully-qualified # 🌸 cherry blossom + "💮", // 1F4AE ; fully-qualified # 💮 white flower + "🌹", // 1F339 ; fully-qualified # 🌹 rose + "🥀", // 1F940 ; fully-qualified # 🥀 wilted flower + "🌺", // 1F33A ; fully-qualified # 🌺 hibiscus + "🌻", // 1F33B ; fully-qualified # 🌻 sunflower + "🌼", // 1F33C ; fully-qualified # 🌼 blossom + "🌷", // 1F337 ; fully-qualified # 🌷 tulip + "🌱", // 1F331 ; fully-qualified # 🌱 seedling + "🌲", // 1F332 ; fully-qualified # 🌲 evergreen tree + "🌳", // 1F333 ; fully-qualified # 🌳 deciduous tree + "🌴", // 1F334 ; fully-qualified # 🌴 palm tree + "🌵", // 1F335 ; fully-qualified # 🌵 cactus + "🌾", // 1F33E ; fully-qualified # 🌾 sheaf of rice + "🌿", // 1F33F ; fully-qualified # 🌿 herb + "🍀", // 1F340 ; fully-qualified # 🍀 four leaf clover + "🍁", // 1F341 ; fully-qualified # 🍁 maple leaf + "🍂", // 1F342 ; fully-qualified # 🍂 fallen leaf + "🍃", // 1F343 ; fully-qualified # 🍃 leaf fluttering in wind + }, + { // # group: Food & Drink + "🍇", // 1F347 ; fully-qualified # 🍇 grapes + "🍈", // 1F348 ; fully-qualified # 🍈 melon + "🍉", // 1F349 ; fully-qualified # 🍉 watermelon + "🍊", // 1F34A ; fully-qualified # 🍊 tangerine + "🍋", // 1F34B ; fully-qualified # 🍋 lemon + "🍌", // 1F34C ; fully-qualified # 🍌 banana + "🍍", // 1F34D ; fully-qualified # 🍍 pineapple + "🥭", // 1F96D ; fully-qualified # 🥭 mango + "🍎", // 1F34E ; fully-qualified # 🍎 red apple + "🍏", // 1F34F ; fully-qualified # 🍏 green apple + "🍐", // 1F350 ; fully-qualified # 🍐 pear + "🍑", // 1F351 ; fully-qualified # 🍑 peach + "🍒", // 1F352 ; fully-qualified # 🍒 cherries + "🍓", // 1F353 ; fully-qualified # 🍓 strawberry + "🥝", // 1F95D ; fully-qualified # 🥝 kiwi fruit + "🍅", // 1F345 ; fully-qualified # 🍅 tomato + "🥥", // 1F965 ; fully-qualified # 🥥 coconut + "🥑", // 1F951 ; fully-qualified # 🥑 avocado + "🍆", // 1F346 ; fully-qualified # 🍆 eggplant + "🥔", // 1F954 ; fully-qualified # 🥔 potato + "🥕", // 1F955 ; fully-qualified # 🥕 carrot + "🌽", // 1F33D ; fully-qualified # 🌽 ear of corn + "🥒", // 1F952 ; fully-qualified # 🥒 cucumber + "🥬", // 1F96C ; fully-qualified # 🥬 leafy green + "🥦", // 1F966 ; fully-qualified # 🥦 broccoli + "🧄", // 1F9C4 ; fully-qualified # 🧄 garlic + "🧅", // 1F9C5 ; fully-qualified # 🧅 onion + "🍄", // 1F344 ; fully-qualified # 🍄 mushroom + "🥜", // 1F95C ; fully-qualified # 🥜 peanuts + "🌰", // 1F330 ; fully-qualified # 🌰 chestnut + "🍞", // 1F35E ; fully-qualified # 🍞 bread + "🥐", // 1F950 ; fully-qualified # 🥐 croissant + "🥖", // 1F956 ; fully-qualified # 🥖 baguette bread + "🥨", // 1F968 ; fully-qualified # 🥨 pretzel + "🥯", // 1F96F ; fully-qualified # 🥯 bagel + "🥞", // 1F95E ; fully-qualified # 🥞 pancakes + "🧇", // 1F9C7 ; fully-qualified # 🧇 waffle + "🧀", // 1F9C0 ; fully-qualified # 🧀 cheese wedge + "🍖", // 1F356 ; fully-qualified # 🍖 meat on bone + "🍗", // 1F357 ; fully-qualified # 🍗 poultry leg + "🥩", // 1F969 ; fully-qualified # 🥩 cut of meat + "🥓", // 1F953 ; fully-qualified # 🥓 bacon + "🍔", // 1F354 ; fully-qualified # 🍔 hamburger + "🍟", // 1F35F ; fully-qualified # 🍟 french fries + "🍕", // 1F355 ; fully-qualified # 🍕 pizza + "🌭", // 1F32D ; fully-qualified # 🌭 hot dog + "🥪", // 1F96A ; fully-qualified # 🥪 sandwich + "🌮", // 1F32E ; fully-qualified # 🌮 taco + "🌯", // 1F32F ; fully-qualified # 🌯 burrito + "🥙", // 1F959 ; fully-qualified # 🥙 stuffed flatbread + "🧆", // 1F9C6 ; fully-qualified # 🧆 falafel + "🥚", // 1F95A ; fully-qualified # 🥚 egg + "🍳", // 1F373 ; fully-qualified # 🍳 cooking + "🥘", // 1F958 ; fully-qualified # 🥘 shallow pan of food + "🍲", // 1F372 ; fully-qualified # 🍲 pot of food + "🥣", // 1F963 ; fully-qualified # 🥣 bowl with spoon + "🥗", // 1F957 ; fully-qualified # 🥗 green salad + "🍿", // 1F37F ; fully-qualified # 🍿 popcorn + "🧈", // 1F9C8 ; fully-qualified # 🧈 butter + "🧂", // 1F9C2 ; fully-qualified # 🧂 salt + "🥫", // 1F96B ; fully-qualified # 🥫 canned food + "🍱", // 1F371 ; fully-qualified # 🍱 bento box + "🍘", // 1F358 ; fully-qualified # 🍘 rice cracker + "🍙", // 1F359 ; fully-qualified # 🍙 rice ball + "🍚", // 1F35A ; fully-qualified # 🍚 cooked rice + "🍛", // 1F35B ; fully-qualified # 🍛 curry rice + "🍜", // 1F35C ; fully-qualified # 🍜 steaming bowl + "🍝", // 1F35D ; fully-qualified # 🍝 spaghetti + "🍠", // 1F360 ; fully-qualified # 🍠 roasted sweet potato + "🍢", // 1F362 ; fully-qualified # 🍢 oden + "🍣", // 1F363 ; fully-qualified # 🍣 sushi + "🍤", // 1F364 ; fully-qualified # 🍤 fried shrimp + "🍥", // 1F365 ; fully-qualified # 🍥 fish cake with swirl + "🥮", // 1F96E ; fully-qualified # 🥮 moon cake + "🍡", // 1F361 ; fully-qualified # 🍡 dango + "🥟", // 1F95F ; fully-qualified # 🥟 dumpling + "🥠", // 1F960 ; fully-qualified # 🥠 fortune cookie + "🥡", // 1F961 ; fully-qualified # 🥡 takeout box + "🦀", // 1F980 ; fully-qualified # 🦀 crab + "🦞", // 1F99E ; fully-qualified # 🦞 lobster + "🦐", // 1F990 ; fully-qualified # 🦐 shrimp + "🦑", // 1F991 ; fully-qualified # 🦑 squid + "🦪", // 1F9AA ; fully-qualified # 🦪 oyster + "🍦", // 1F366 ; fully-qualified # 🍦 soft ice cream + "🍧", // 1F367 ; fully-qualified # 🍧 shaved ice + "🍨", // 1F368 ; fully-qualified # 🍨 ice cream + "🍩", // 1F369 ; fully-qualified # 🍩 doughnut + "🍪", // 1F36A ; fully-qualified # 🍪 cookie + "🎂", // 1F382 ; fully-qualified # 🎂 birthday cake + "🍰", // 1F370 ; fully-qualified # 🍰 shortcake + "🧁", // 1F9C1 ; fully-qualified # 🧁 cupcake + "🥧", // 1F967 ; fully-qualified # 🥧 pie + "🍫", // 1F36B ; fully-qualified # 🍫 chocolate bar + "🍬", // 1F36C ; fully-qualified # 🍬 candy + "🍭", // 1F36D ; fully-qualified # 🍭 lollipop + "🍮", // 1F36E ; fully-qualified # 🍮 custard + "🍯", // 1F36F ; fully-qualified # 🍯 honey pot + "🍼", // 1F37C ; fully-qualified # 🍼 baby bottle + "🥛", // 1F95B ; fully-qualified # 🥛 glass of milk + "☕", // 2615 ; fully-qualified # ☕ hot beverage + "🍵", // 1F375 ; fully-qualified # 🍵 teacup without handle + "🍶", // 1F376 ; fully-qualified # 🍶 sake + "🍾", // 1F37E ; fully-qualified # 🍾 bottle with popping cork + "🍷", // 1F377 ; fully-qualified # 🍷 wine glass + "🍸", // 1F378 ; fully-qualified # 🍸 cocktail glass + "🍹", // 1F379 ; fully-qualified # 🍹 tropical drink + "🍺", // 1F37A ; fully-qualified # 🍺 beer mug + "🍻", // 1F37B ; fully-qualified # 🍻 clinking beer mugs + "🥂", // 1F942 ; fully-qualified # 🥂 clinking glasses + "🥃", // 1F943 ; fully-qualified # 🥃 tumbler glass + "🥤", // 1F964 ; fully-qualified # 🥤 cup with straw + "🧃", // 1F9C3 ; fully-qualified # 🧃 beverage box + "🧉", // 1F9C9 ; fully-qualified # 🧉 mate + "🧊", // 1F9CA ; fully-qualified # 🧊 ice cube + "🥢", // 1F962 ; fully-qualified # 🥢 chopsticks + "🍴", // 1F374 ; fully-qualified # 🍴 fork and knife + "🥄", // 1F944 ; fully-qualified # 🥄 spoon + "🔪", // 1F52A ; fully-qualified # 🔪 kitchen knife + "🏺", // 1F3FA ; fully-qualified # 🏺 amphora + }, + { // # group: Travel & Places + "🌍", // 1F30D ; fully-qualified # 🌍 globe showing Europe-Africa + "🌎", // 1F30E ; fully-qualified # 🌎 globe showing Americas + "🌏", // 1F30F ; fully-qualified # 🌏 globe showing Asia-Australia + "🌐", // 1F310 ; fully-qualified # 🌐 globe with meridians + "🗾", // 1F5FE ; fully-qualified # 🗾 map of Japan + "🧭", // 1F9ED ; fully-qualified # 🧭 compass + "🌋", // 1F30B ; fully-qualified # 🌋 volcano + "🗻", // 1F5FB ; fully-qualified # 🗻 mount fuji + "🧱", // 1F9F1 ; fully-qualified # 🧱 brick + "🏠", // 1F3E0 ; fully-qualified # 🏠 house + "🏡", // 1F3E1 ; fully-qualified # 🏡 house with garden + "🏢", // 1F3E2 ; fully-qualified # 🏢 office building + "🏣", // 1F3E3 ; fully-qualified # 🏣 Japanese post office + "🏤", // 1F3E4 ; fully-qualified # 🏤 post office + "🏥", // 1F3E5 ; fully-qualified # 🏥 hospital + "🏦", // 1F3E6 ; fully-qualified # 🏦 bank + "🏨", // 1F3E8 ; fully-qualified # 🏨 hotel + "🏩", // 1F3E9 ; fully-qualified # 🏩 love hotel + "🏪", // 1F3EA ; fully-qualified # 🏪 convenience store + "🏫", // 1F3EB ; fully-qualified # 🏫 school + "🏬", // 1F3EC ; fully-qualified # 🏬 department store + "🏭", // 1F3ED ; fully-qualified # 🏭 factory + "🏯", // 1F3EF ; fully-qualified # 🏯 Japanese castle + "🏰", // 1F3F0 ; fully-qualified # 🏰 castle + "💒", // 1F492 ; fully-qualified # 💒 wedding + "🗼", // 1F5FC ; fully-qualified # 🗼 Tokyo tower + "🗽", // 1F5FD ; fully-qualified # 🗽 Statue of Liberty + "⛪", // 26EA ; fully-qualified # ⛪ church + "🕌", // 1F54C ; fully-qualified # 🕌 mosque + "🛕", // 1F6D5 ; fully-qualified # 🛕 hindu temple + "🕍", // 1F54D ; fully-qualified # 🕍 synagogue + "🕋", // 1F54B ; fully-qualified # 🕋 kaaba + "⛲", // 26F2 ; fully-qualified # ⛲ fountain + "⛺", // 26FA ; fully-qualified # ⛺ tent + "🌁", // 1F301 ; fully-qualified # 🌁 foggy + "🌃", // 1F303 ; fully-qualified # 🌃 night with stars + "🌄", // 1F304 ; fully-qualified # 🌄 sunrise over mountains + "🌅", // 1F305 ; fully-qualified # 🌅 sunrise + "🌆", // 1F306 ; fully-qualified # 🌆 cityscape at dusk + "🌇", // 1F307 ; fully-qualified # 🌇 sunset + "🌉", // 1F309 ; fully-qualified # 🌉 bridge at night + "🎠", // 1F3A0 ; fully-qualified # 🎠 carousel horse + "🎡", // 1F3A1 ; fully-qualified # 🎡 ferris wheel + "🎢", // 1F3A2 ; fully-qualified # 🎢 roller coaster + "💈", // 1F488 ; fully-qualified # 💈 barber pole + "🎪", // 1F3AA ; fully-qualified # 🎪 circus tent + "🚂", // 1F682 ; fully-qualified # 🚂 locomotive + "🚃", // 1F683 ; fully-qualified # 🚃 railway car + "🚄", // 1F684 ; fully-qualified # 🚄 high-speed train + "🚅", // 1F685 ; fully-qualified # 🚅 bullet train + "🚆", // 1F686 ; fully-qualified # 🚆 train + "🚇", // 1F687 ; fully-qualified # 🚇 metro + "🚈", // 1F688 ; fully-qualified # 🚈 light rail + "🚉", // 1F689 ; fully-qualified # 🚉 station + "🚊", // 1F68A ; fully-qualified # 🚊 tram + "🚝", // 1F69D ; fully-qualified # 🚝 monorail + "🚞", // 1F69E ; fully-qualified # 🚞 mountain railway + "🚋", // 1F68B ; fully-qualified # 🚋 tram car + "🚌", // 1F68C ; fully-qualified # 🚌 bus + "🚍", // 1F68D ; fully-qualified # 🚍 oncoming bus + "🚎", // 1F68E ; fully-qualified # 🚎 trolleybus + "🚐", // 1F690 ; fully-qualified # 🚐 minibus + "🚑", // 1F691 ; fully-qualified # 🚑 ambulance + "🚒", // 1F692 ; fully-qualified # 🚒 fire engine + "🚓", // 1F693 ; fully-qualified # 🚓 police car + "🚔", // 1F694 ; fully-qualified # 🚔 oncoming police car + "🚕", // 1F695 ; fully-qualified # 🚕 taxi + "🚖", // 1F696 ; fully-qualified # 🚖 oncoming taxi + "🚗", // 1F697 ; fully-qualified # 🚗 automobile + "🚘", // 1F698 ; fully-qualified # 🚘 oncoming automobile + "🚙", // 1F699 ; fully-qualified # 🚙 sport utility vehicle + "🚚", // 1F69A ; fully-qualified # 🚚 delivery truck + "🚛", // 1F69B ; fully-qualified # 🚛 articulated lorry + "🚜", // 1F69C ; fully-qualified # 🚜 tractor + "🛵", // 1F6F5 ; fully-qualified # 🛵 motor scooter + "🦽", // 1F9BD ; fully-qualified # 🦽 manual wheelchair + "🦼", // 1F9BC ; fully-qualified # 🦼 motorized wheelchair + "🛺", // 1F6FA ; fully-qualified # 🛺 auto rickshaw + "🚲", // 1F6B2 ; fully-qualified # 🚲 bicycle + "🛴", // 1F6F4 ; fully-qualified # 🛴 kick scooter + "🛹", // 1F6F9 ; fully-qualified # 🛹 skateboard + "🚏", // 1F68F ; fully-qualified # 🚏 bus stop + "⛽", // 26FD ; fully-qualified # ⛽ fuel pump + "🚨", // 1F6A8 ; fully-qualified # 🚨 police car light + "🚥", // 1F6A5 ; fully-qualified # 🚥 horizontal traffic light + "🚦", // 1F6A6 ; fully-qualified # 🚦 vertical traffic light + "🛑", // 1F6D1 ; fully-qualified # 🛑 stop sign + "🚧", // 1F6A7 ; fully-qualified # 🚧 construction + "⚓", // 2693 ; fully-qualified # ⚓ anchor + "⛵", // 26F5 ; fully-qualified # ⛵ sailboat + "🛶", // 1F6F6 ; fully-qualified # 🛶 canoe + "🚤", // 1F6A4 ; fully-qualified # 🚤 speedboat + "🚢", // 1F6A2 ; fully-qualified # 🚢 ship + "🛫", // 1F6EB ; fully-qualified # 🛫 airplane departure + "🛬", // 1F6EC ; fully-qualified # 🛬 airplane arrival + "🪂", // 1FA82 ; fully-qualified # 🪂 parachute + "💺", // 1F4BA ; fully-qualified # 💺 seat + "🚁", // 1F681 ; fully-qualified # 🚁 helicopter + "🚟", // 1F69F ; fully-qualified # 🚟 suspension railway + "🚠", // 1F6A0 ; fully-qualified # 🚠 mountain cableway + "🚡", // 1F6A1 ; fully-qualified # 🚡 aerial tramway + "🚀", // 1F680 ; fully-qualified # 🚀 rocket + "🛸", // 1F6F8 ; fully-qualified # 🛸 flying saucer + "🧳", // 1F9F3 ; fully-qualified # 🧳 luggage + "⌛", // 231B ; fully-qualified # ⌛ hourglass done + "⏳", // 23F3 ; fully-qualified # ⏳ hourglass not done + "⌚", // 231A ; fully-qualified # ⌚ watch + "⏰", // 23F0 ; fully-qualified # ⏰ alarm clock + "🕛", // 1F55B ; fully-qualified # 🕛 twelve o’clock + "🕧", // 1F567 ; fully-qualified # 🕧 twelve-thirty + "🕐", // 1F550 ; fully-qualified # 🕐 one o’clock + "🕜", // 1F55C ; fully-qualified # 🕜 one-thirty + "🕑", // 1F551 ; fully-qualified # 🕑 two o’clock + "🕝", // 1F55D ; fully-qualified # 🕝 two-thirty + "🕒", // 1F552 ; fully-qualified # 🕒 three o’clock + "🕞", // 1F55E ; fully-qualified # 🕞 three-thirty + "🕓", // 1F553 ; fully-qualified # 🕓 four o’clock + "🕟", // 1F55F ; fully-qualified # 🕟 four-thirty + "🕔", // 1F554 ; fully-qualified # 🕔 five o’clock + "🕠", // 1F560 ; fully-qualified # 🕠 five-thirty + "🕕", // 1F555 ; fully-qualified # 🕕 six o’clock + "🕡", // 1F561 ; fully-qualified # 🕡 six-thirty + "🕖", // 1F556 ; fully-qualified # 🕖 seven o’clock + "🕢", // 1F562 ; fully-qualified # 🕢 seven-thirty + "🕗", // 1F557 ; fully-qualified # 🕗 eight o’clock + "🕣", // 1F563 ; fully-qualified # 🕣 eight-thirty + "🕘", // 1F558 ; fully-qualified # 🕘 nine o’clock + "🕤", // 1F564 ; fully-qualified # 🕤 nine-thirty + "🕙", // 1F559 ; fully-qualified # 🕙 ten o’clock + "🕥", // 1F565 ; fully-qualified # 🕥 ten-thirty + "🕚", // 1F55A ; fully-qualified # 🕚 eleven o’clock + "🕦", // 1F566 ; fully-qualified # 🕦 eleven-thirty + "🌑", // 1F311 ; fully-qualified # 🌑 new moon + "🌒", // 1F312 ; fully-qualified # 🌒 waxing crescent moon + "🌓", // 1F313 ; fully-qualified # 🌓 first quarter moon + "🌔", // 1F314 ; fully-qualified # 🌔 waxing gibbous moon + "🌕", // 1F315 ; fully-qualified # 🌕 full moon + "🌖", // 1F316 ; fully-qualified # 🌖 waning gibbous moon + "🌗", // 1F317 ; fully-qualified # 🌗 last quarter moon + "🌘", // 1F318 ; fully-qualified # 🌘 waning crescent moon + "🌙", // 1F319 ; fully-qualified # 🌙 crescent moon + "🌚", // 1F31A ; fully-qualified # 🌚 new moon face + "🌛", // 1F31B ; fully-qualified # 🌛 first quarter moon face + "🌜", // 1F31C ; fully-qualified # 🌜 last quarter moon face + "🌝", // 1F31D ; fully-qualified # 🌝 full moon face + "🌞", // 1F31E ; fully-qualified # 🌞 sun with face + "🪐", // 1FA90 ; fully-qualified # 🪐 ringed planet + "⭐", // 2B50 ; fully-qualified # ⭐ star + "🌟", // 1F31F ; fully-qualified # 🌟 glowing star + "🌠", // 1F320 ; fully-qualified # 🌠 shooting star + "🌌", // 1F30C ; fully-qualified # 🌌 milky way + "⛅", // 26C5 ; fully-qualified # ⛅ sun behind cloud + "🌀", // 1F300 ; fully-qualified # 🌀 cyclone + "🌈", // 1F308 ; fully-qualified # 🌈 rainbow + "🌂", // 1F302 ; fully-qualified # 🌂 closed umbrella + "☔", // 2614 ; fully-qualified # ☔ umbrella with rain drops + "⚡", // 26A1 ; fully-qualified # ⚡ high voltage + "⛄", // 26C4 ; fully-qualified # ⛄ snowman without snow + "🔥", // 1F525 ; fully-qualified # 🔥 fire + "💧", // 1F4A7 ; fully-qualified # 💧 droplet + "🌊", // 1F30A ; fully-qualified # 🌊 water wave + }, + { // # group: Activities + "🎃", // 1F383 ; fully-qualified # 🎃 jack-o-lantern + "🎄", // 1F384 ; fully-qualified # 🎄 Christmas tree + "🎆", // 1F386 ; fully-qualified # 🎆 fireworks + "🎇", // 1F387 ; fully-qualified # 🎇 sparkler + "🧨", // 1F9E8 ; fully-qualified # 🧨 firecracker + "✨", // 2728 ; fully-qualified # ✨ sparkles + "🎈", // 1F388 ; fully-qualified # 🎈 balloon + "🎉", // 1F389 ; fully-qualified # 🎉 party popper + "🎊", // 1F38A ; fully-qualified # 🎊 confetti ball + "🎋", // 1F38B ; fully-qualified # 🎋 tanabata tree + "🎍", // 1F38D ; fully-qualified # 🎍 pine decoration + "🎎", // 1F38E ; fully-qualified # 🎎 Japanese dolls + "🎏", // 1F38F ; fully-qualified # 🎏 carp streamer + "🎐", // 1F390 ; fully-qualified # 🎐 wind chime + "🎑", // 1F391 ; fully-qualified # 🎑 moon viewing ceremony + "🧧", // 1F9E7 ; fully-qualified # 🧧 red envelope + "🎀", // 1F380 ; fully-qualified # 🎀 ribbon + "🎁", // 1F381 ; fully-qualified # 🎁 wrapped gift + "🎫", // 1F3AB ; fully-qualified # 🎫 ticket + "🏆", // 1F3C6 ; fully-qualified # 🏆 trophy + "🏅", // 1F3C5 ; fully-qualified # 🏅 sports medal + "🥇", // 1F947 ; fully-qualified # 🥇 1st place medal + "🥈", // 1F948 ; fully-qualified # 🥈 2nd place medal + "🥉", // 1F949 ; fully-qualified # 🥉 3rd place medal + "⚽", // 26BD ; fully-qualified # ⚽ soccer ball + "⚾", // 26BE ; fully-qualified # ⚾ baseball + "🥎", // 1F94E ; fully-qualified # 🥎 softball + "🏀", // 1F3C0 ; fully-qualified # 🏀 basketball + "🏐", // 1F3D0 ; fully-qualified # 🏐 volleyball + "🏈", // 1F3C8 ; fully-qualified # 🏈 american football + "🏉", // 1F3C9 ; fully-qualified # 🏉 rugby football + "🎾", // 1F3BE ; fully-qualified # 🎾 tennis + "🥏", // 1F94F ; fully-qualified # 🥏 flying disc + "🎳", // 1F3B3 ; fully-qualified # 🎳 bowling + "🏏", // 1F3CF ; fully-qualified # 🏏 cricket game + "🏑", // 1F3D1 ; fully-qualified # 🏑 field hockey + "🏒", // 1F3D2 ; fully-qualified # 🏒 ice hockey + "🥍", // 1F94D ; fully-qualified # 🥍 lacrosse + "🏓", // 1F3D3 ; fully-qualified # 🏓 ping pong + "🏸", // 1F3F8 ; fully-qualified # 🏸 badminton + "🥊", // 1F94A ; fully-qualified # 🥊 boxing glove + "🥋", // 1F94B ; fully-qualified # 🥋 martial arts uniform + "🥅", // 1F945 ; fully-qualified # 🥅 goal net + "⛳", // 26F3 ; fully-qualified # ⛳ flag in hole + "🎣", // 1F3A3 ; fully-qualified # 🎣 fishing pole + "🤿", // 1F93F ; fully-qualified # 🤿 diving mask + "🎽", // 1F3BD ; fully-qualified # 🎽 running shirt + "🎿", // 1F3BF ; fully-qualified # 🎿 skis + "🛷", // 1F6F7 ; fully-qualified # 🛷 sled + "🥌", // 1F94C ; fully-qualified # 🥌 curling stone + "🎯", // 1F3AF ; fully-qualified # 🎯 direct hit + "🪀", // 1FA80 ; fully-qualified # 🪀 yo-yo + "🪁", // 1FA81 ; fully-qualified # 🪁 kite + "🎱", // 1F3B1 ; fully-qualified # 🎱 pool 8 ball + "🔮", // 1F52E ; fully-qualified # 🔮 crystal ball + "🧿", // 1F9FF ; fully-qualified # 🧿 nazar amulet + "🎮", // 1F3AE ; fully-qualified # 🎮 video game + "🎰", // 1F3B0 ; fully-qualified # 🎰 slot machine + "🎲", // 1F3B2 ; fully-qualified # 🎲 game die + "🧩", // 1F9E9 ; fully-qualified # 🧩 puzzle piece + "🧸", // 1F9F8 ; fully-qualified # 🧸 teddy bear + "🃏", // 1F0CF ; fully-qualified # 🃏 joker + "🀄", // 1F004 ; fully-qualified # 🀄 mahjong red dragon + "🎴", // 1F3B4 ; fully-qualified # 🎴 flower playing cards + "🎭", // 1F3AD ; fully-qualified # 🎭 performing arts + "🎨", // 1F3A8 ; fully-qualified # 🎨 artist palette + "🧵", // 1F9F5 ; fully-qualified # 🧵 thread + "🧶", // 1F9F6 ; fully-qualified # 🧶 yarn + }, + { // # group: Objects + "👓", // 1F453 ; fully-qualified # 👓 glasses + "🥽", // 1F97D ; fully-qualified # 🥽 goggles + "🥼", // 1F97C ; fully-qualified # 🥼 lab coat + "🦺", // 1F9BA ; fully-qualified # 🦺 safety vest + "👔", // 1F454 ; fully-qualified # 👔 necktie + "👕", // 1F455 ; fully-qualified # 👕 t-shirt + "👖", // 1F456 ; fully-qualified # 👖 jeans + "🧣", // 1F9E3 ; fully-qualified # 🧣 scarf + "🧤", // 1F9E4 ; fully-qualified # 🧤 gloves + "🧥", // 1F9E5 ; fully-qualified # 🧥 coat + "🧦", // 1F9E6 ; fully-qualified # 🧦 socks + "👗", // 1F457 ; fully-qualified # 👗 dress + "👘", // 1F458 ; fully-qualified # 👘 kimono + "🥻", // 1F97B ; fully-qualified # 🥻 sari + "🩱", // 1FA71 ; fully-qualified # 🩱 one-piece swimsuit + "🩲", // 1FA72 ; fully-qualified # 🩲 swim brief + "🩳", // 1FA73 ; fully-qualified # 🩳 shorts + "👙", // 1F459 ; fully-qualified # 👙 bikini + "👚", // 1F45A ; fully-qualified # 👚 woman’s clothes + "👛", // 1F45B ; fully-qualified # 👛 purse + "👜", // 1F45C ; fully-qualified # 👜 handbag + "👝", // 1F45D ; fully-qualified # 👝 clutch bag + "🎒", // 1F392 ; fully-qualified # 🎒 backpack + "👞", // 1F45E ; fully-qualified # 👞 man’s shoe + "👟", // 1F45F ; fully-qualified # 👟 running shoe + "🥾", // 1F97E ; fully-qualified # 🥾 hiking boot + "🥿", // 1F97F ; fully-qualified # 🥿 flat shoe + "👠", // 1F460 ; fully-qualified # 👠 high-heeled shoe + "👡", // 1F461 ; fully-qualified # 👡 woman’s sandal + "🩰", // 1FA70 ; fully-qualified # 🩰 ballet shoes + "👢", // 1F462 ; fully-qualified # 👢 woman’s boot + "👑", // 1F451 ; fully-qualified # 👑 crown + "👒", // 1F452 ; fully-qualified # 👒 woman’s hat + "🎩", // 1F3A9 ; fully-qualified # 🎩 top hat + "🎓", // 1F393 ; fully-qualified # 🎓 graduation cap + "🧢", // 1F9E2 ; fully-qualified # 🧢 billed cap + "📿", // 1F4FF ; fully-qualified # 📿 prayer beads + "💄", // 1F484 ; fully-qualified # 💄 lipstick + "💍", // 1F48D ; fully-qualified # 💍 ring + "💎", // 1F48E ; fully-qualified # 💎 gem stone + "🔇", // 1F507 ; fully-qualified # 🔇 muted speaker + "🔈", // 1F508 ; fully-qualified # 🔈 speaker low volume + "🔉", // 1F509 ; fully-qualified # 🔉 speaker medium volume + "🔊", // 1F50A ; fully-qualified # 🔊 speaker high volume + "📢", // 1F4E2 ; fully-qualified # 📢 loudspeaker + "📣", // 1F4E3 ; fully-qualified # 📣 megaphone + "📯", // 1F4EF ; fully-qualified # 📯 postal horn + "🔔", // 1F514 ; fully-qualified # 🔔 bell + "🔕", // 1F515 ; fully-qualified # 🔕 bell with slash + "🎼", // 1F3BC ; fully-qualified # 🎼 musical score + "🎵", // 1F3B5 ; fully-qualified # 🎵 musical note + "🎶", // 1F3B6 ; fully-qualified # 🎶 musical notes + "🎤", // 1F3A4 ; fully-qualified # 🎤 microphone + "🎧", // 1F3A7 ; fully-qualified # 🎧 headphone + "📻", // 1F4FB ; fully-qualified # 📻 radio + "🎷", // 1F3B7 ; fully-qualified # 🎷 saxophone + "🎸", // 1F3B8 ; fully-qualified # 🎸 guitar + "🎹", // 1F3B9 ; fully-qualified # 🎹 musical keyboard + "🎺", // 1F3BA ; fully-qualified # 🎺 trumpet + "🎻", // 1F3BB ; fully-qualified # 🎻 violin + "🪕", // 1FA95 ; fully-qualified # 🪕 banjo + "🥁", // 1F941 ; fully-qualified # 🥁 drum + "📱", // 1F4F1 ; fully-qualified # 📱 mobile phone + "📲", // 1F4F2 ; fully-qualified # 📲 mobile phone with arrow + "📞", // 1F4DE ; fully-qualified # 📞 telephone receiver + "📟", // 1F4DF ; fully-qualified # 📟 pager + "📠", // 1F4E0 ; fully-qualified # 📠 fax machine + "🔋", // 1F50B ; fully-qualified # 🔋 battery + "🔌", // 1F50C ; fully-qualified # 🔌 electric plug + "💻", // 1F4BB ; fully-qualified # 💻 laptop computer + "💽", // 1F4BD ; fully-qualified # 💽 computer disk + "💾", // 1F4BE ; fully-qualified # 💾 floppy disk + "💿", // 1F4BF ; fully-qualified # 💿 optical disk + "📀", // 1F4C0 ; fully-qualified # 📀 dvd + "🧮", // 1F9EE ; fully-qualified # 🧮 abacus + "🎥", // 1F3A5 ; fully-qualified # 🎥 movie camera + "🎬", // 1F3AC ; fully-qualified # 🎬 clapper board + "📺", // 1F4FA ; fully-qualified # 📺 television + "📷", // 1F4F7 ; fully-qualified # 📷 camera + "📸", // 1F4F8 ; fully-qualified # 📸 camera with flash + "📹", // 1F4F9 ; fully-qualified # 📹 video camera + "📼", // 1F4FC ; fully-qualified # 📼 videocassette + "🔍", // 1F50D ; fully-qualified # 🔍 magnifying glass tilted left + "🔎", // 1F50E ; fully-qualified # 🔎 magnifying glass tilted right + "💡", // 1F4A1 ; fully-qualified # 💡 light bulb + "🔦", // 1F526 ; fully-qualified # 🔦 flashlight + "🏮", // 1F3EE ; fully-qualified # 🏮 red paper lantern + "🪔", // 1FA94 ; fully-qualified # 🪔 diya lamp + "📔", // 1F4D4 ; fully-qualified # 📔 notebook with decorative cover + "📕", // 1F4D5 ; fully-qualified # 📕 closed book + "📖", // 1F4D6 ; fully-qualified # 📖 open book + "📗", // 1F4D7 ; fully-qualified # 📗 green book + "📘", // 1F4D8 ; fully-qualified # 📘 blue book + "📙", // 1F4D9 ; fully-qualified # 📙 orange book + "📚", // 1F4DA ; fully-qualified # 📚 books + "📓", // 1F4D3 ; fully-qualified # 📓 notebook + "📒", // 1F4D2 ; fully-qualified # 📒 ledger + "📃", // 1F4C3 ; fully-qualified # 📃 page with curl + "📜", // 1F4DC ; fully-qualified # 📜 scroll + "📄", // 1F4C4 ; fully-qualified # 📄 page facing up + "📰", // 1F4F0 ; fully-qualified # 📰 newspaper + "📑", // 1F4D1 ; fully-qualified # 📑 bookmark tabs + "🔖", // 1F516 ; fully-qualified # 🔖 bookmark + "💰", // 1F4B0 ; fully-qualified # 💰 money bag + "💴", // 1F4B4 ; fully-qualified # 💴 yen banknote + "💵", // 1F4B5 ; fully-qualified # 💵 dollar banknote + "💶", // 1F4B6 ; fully-qualified # 💶 euro banknote + "💷", // 1F4B7 ; fully-qualified # 💷 pound banknote + "💸", // 1F4B8 ; fully-qualified # 💸 money with wings + "💳", // 1F4B3 ; fully-qualified # 💳 credit card + "🧾", // 1F9FE ; fully-qualified # 🧾 receipt + "💹", // 1F4B9 ; fully-qualified # 💹 chart increasing with yen + "💱", // 1F4B1 ; fully-qualified # 💱 currency exchange + "💲", // 1F4B2 ; fully-qualified # 💲 heavy dollar sign + "📧", // 1F4E7 ; fully-qualified # 📧 e-mail + "📨", // 1F4E8 ; fully-qualified # 📨 incoming envelope + "📩", // 1F4E9 ; fully-qualified # 📩 envelope with arrow + "📤", // 1F4E4 ; fully-qualified # 📤 outbox tray + "📥", // 1F4E5 ; fully-qualified # 📥 inbox tray + "📦", // 1F4E6 ; fully-qualified # 📦 package + "📫", // 1F4EB ; fully-qualified # 📫 closed mailbox with raised flag + "📪", // 1F4EA ; fully-qualified # 📪 closed mailbox with lowered flag + "📬", // 1F4EC ; fully-qualified # 📬 open mailbox with raised flag + "📭", // 1F4ED ; fully-qualified # 📭 open mailbox with lowered flag + "📮", // 1F4EE ; fully-qualified # 📮 postbox + "📝", // 1F4DD ; fully-qualified # 📝 memo + "💼", // 1F4BC ; fully-qualified # 💼 briefcase + "📁", // 1F4C1 ; fully-qualified # 📁 file folder + "📂", // 1F4C2 ; fully-qualified # 📂 open file folder + "📅", // 1F4C5 ; fully-qualified # 📅 calendar + "📆", // 1F4C6 ; fully-qualified # 📆 tear-off calendar + "📇", // 1F4C7 ; fully-qualified # 📇 card index + "📈", // 1F4C8 ; fully-qualified # 📈 chart increasing + "📉", // 1F4C9 ; fully-qualified # 📉 chart decreasing + "📊", // 1F4CA ; fully-qualified # 📊 bar chart + "📋", // 1F4CB ; fully-qualified # 📋 clipboard + "📌", // 1F4CC ; fully-qualified # 📌 pushpin + "📍", // 1F4CD ; fully-qualified # 📍 round pushpin + "📎", // 1F4CE ; fully-qualified # 📎 paperclip + "📏", // 1F4CF ; fully-qualified # 📏 straight ruler + "📐", // 1F4D0 ; fully-qualified # 📐 triangular ruler + "🔒", // 1F512 ; fully-qualified # 🔒 locked + "🔓", // 1F513 ; fully-qualified # 🔓 unlocked + "🔏", // 1F50F ; fully-qualified # 🔏 locked with pen + "🔐", // 1F510 ; fully-qualified # 🔐 locked with key + "🔑", // 1F511 ; fully-qualified # 🔑 key + "🔨", // 1F528 ; fully-qualified # 🔨 hammer + "🪓", // 1FA93 ; fully-qualified # 🪓 axe + "🔫", // 1F52B ; fully-qualified # 🔫 pistol + "🏹", // 1F3F9 ; fully-qualified # 🏹 bow and arrow + "🔧", // 1F527 ; fully-qualified # 🔧 wrench + "🔩", // 1F529 ; fully-qualified # 🔩 nut and bolt + "🦯", // 1F9AF ; fully-qualified # 🦯 probing cane + "🔗", // 1F517 ; fully-qualified # 🔗 link + "🧰", // 1F9F0 ; fully-qualified # 🧰 toolbox + "🧲", // 1F9F2 ; fully-qualified # 🧲 magnet + "🧪", // 1F9EA ; fully-qualified # 🧪 test tube + "🧫", // 1F9EB ; fully-qualified # 🧫 petri dish + "🧬", // 1F9EC ; fully-qualified # 🧬 dna + "🔬", // 1F52C ; fully-qualified # 🔬 microscope + "🔭", // 1F52D ; fully-qualified # 🔭 telescope + "📡", // 1F4E1 ; fully-qualified # 📡 satellite antenna + "💉", // 1F489 ; fully-qualified # 💉 syringe + "🩸", // 1FA78 ; fully-qualified # 🩸 drop of blood + "💊", // 1F48A ; fully-qualified # 💊 pill + "🩹", // 1FA79 ; fully-qualified # 🩹 adhesive bandage + "🩺", // 1FA7A ; fully-qualified # 🩺 stethoscope + "🚪", // 1F6AA ; fully-qualified # 🚪 door + "🪑", // 1FA91 ; fully-qualified # 🪑 chair + "🚽", // 1F6BD ; fully-qualified # 🚽 toilet + "🚿", // 1F6BF ; fully-qualified # 🚿 shower + "🛁", // 1F6C1 ; fully-qualified # 🛁 bathtub + "🪒", // 1FA92 ; fully-qualified # 🪒 razor + "🧴", // 1F9F4 ; fully-qualified # 🧴 lotion bottle + "🧷", // 1F9F7 ; fully-qualified # 🧷 safety pin + "🧹", // 1F9F9 ; fully-qualified # 🧹 broom + "🧺", // 1F9FA ; fully-qualified # 🧺 basket + "🧻", // 1F9FB ; fully-qualified # 🧻 roll of paper + "🧼", // 1F9FC ; fully-qualified # 🧼 soap + "🧽", // 1F9FD ; fully-qualified # 🧽 sponge + "🧯", // 1F9EF ; fully-qualified # 🧯 fire extinguisher + "🛒", // 1F6D2 ; fully-qualified # 🛒 shopping cart + "🚬", // 1F6AC ; fully-qualified # 🚬 cigarette + "🗿", // 1F5FF ; fully-qualified # 🗿 moai + }, + { // # group: Symbols + "🏧", // 1F3E7 ; fully-qualified # 🏧 ATM sign + "🚮", // 1F6AE ; fully-qualified # 🚮 litter in bin sign + "🚰", // 1F6B0 ; fully-qualified # 🚰 potable water + "♿", // 267F ; fully-qualified # ♿ wheelchair symbol + "🚹", // 1F6B9 ; fully-qualified # 🚹 men’s room + "🚺", // 1F6BA ; fully-qualified # 🚺 women’s room + "🚻", // 1F6BB ; fully-qualified # 🚻 restroom + "🚼", // 1F6BC ; fully-qualified # 🚼 baby symbol + "🚾", // 1F6BE ; fully-qualified # 🚾 water closet + "🛂", // 1F6C2 ; fully-qualified # 🛂 passport control + "🛃", // 1F6C3 ; fully-qualified # 🛃 customs + "🛄", // 1F6C4 ; fully-qualified # 🛄 baggage claim + "🛅", // 1F6C5 ; fully-qualified # 🛅 left luggage + "🚸", // 1F6B8 ; fully-qualified # 🚸 children crossing + "⛔", // 26D4 ; fully-qualified # ⛔ no entry + "🚫", // 1F6AB ; fully-qualified # 🚫 prohibited + "🚳", // 1F6B3 ; fully-qualified # 🚳 no bicycles + "🚭", // 1F6AD ; fully-qualified # 🚭 no smoking + "🚯", // 1F6AF ; fully-qualified # 🚯 no littering + "🚱", // 1F6B1 ; fully-qualified # 🚱 non-potable water + "🚷", // 1F6B7 ; fully-qualified # 🚷 no pedestrians + "📵", // 1F4F5 ; fully-qualified # 📵 no mobile phones + "🔞", // 1F51E ; fully-qualified # 🔞 no one under eighteen + "🔃", // 1F503 ; fully-qualified # 🔃 clockwise vertical arrows + "🔄", // 1F504 ; fully-qualified # 🔄 counterclockwise arrows button + "🔙", // 1F519 ; fully-qualified # 🔙 BACK arrow + "🔚", // 1F51A ; fully-qualified # 🔚 END arrow + "🔛", // 1F51B ; fully-qualified # 🔛 ON! arrow + "🔜", // 1F51C ; fully-qualified # 🔜 SOON arrow + "🔝", // 1F51D ; fully-qualified # 🔝 TOP arrow + "🛐", // 1F6D0 ; fully-qualified # 🛐 place of worship + "🕎", // 1F54E ; fully-qualified # 🕎 menorah + "🔯", // 1F52F ; fully-qualified # 🔯 dotted six-pointed star + "♈", // 2648 ; fully-qualified # ♈ Aries + "♉", // 2649 ; fully-qualified # ♉ Taurus + "♊", // 264A ; fully-qualified # ♊ Gemini + "♋", // 264B ; fully-qualified # ♋ Cancer + "♌", // 264C ; fully-qualified # ♌ Leo + "♍", // 264D ; fully-qualified # ♍ Virgo + "♎", // 264E ; fully-qualified # ♎ Libra + "♏", // 264F ; fully-qualified # ♏ Scorpio + "♐", // 2650 ; fully-qualified # ♐ Sagittarius + "♑", // 2651 ; fully-qualified # ♑ Capricorn + "♒", // 2652 ; fully-qualified # ♒ Aquarius + "♓", // 2653 ; fully-qualified # ♓ Pisces + "⛎", // 26CE ; fully-qualified # ⛎ Ophiuchus + "🔀", // 1F500 ; fully-qualified # 🔀 shuffle tracks button + "🔁", // 1F501 ; fully-qualified # 🔁 repeat button + "🔂", // 1F502 ; fully-qualified # 🔂 repeat single button + "⏩", // 23E9 ; fully-qualified # ⏩ fast-forward button + "⏪", // 23EA ; fully-qualified # ⏪ fast reverse button + "🔼", // 1F53C ; fully-qualified # 🔼 upwards button + "⏫", // 23EB ; fully-qualified # ⏫ fast up button + "🔽", // 1F53D ; fully-qualified # 🔽 downwards button + "⏬", // 23EC ; fully-qualified # ⏬ fast down button + "🎦", // 1F3A6 ; fully-qualified # 🎦 cinema + "🔅", // 1F505 ; fully-qualified # 🔅 dim button + "🔆", // 1F506 ; fully-qualified # 🔆 bright button + "📶", // 1F4F6 ; fully-qualified # 📶 antenna bars + "📳", // 1F4F3 ; fully-qualified # 📳 vibration mode + "📴", // 1F4F4 ; fully-qualified # 📴 mobile phone off + "🔱", // 1F531 ; fully-qualified # 🔱 trident emblem + "📛", // 1F4DB ; fully-qualified # 📛 name badge + "🔰", // 1F530 ; fully-qualified # 🔰 Japanese symbol for beginner + "⭕", // 2B55 ; fully-qualified # ⭕ hollow red circle + "✅", // 2705 ; fully-qualified # ✅ check mark button + "❌", // 274C ; fully-qualified # ❌ cross mark + "❎", // 274E ; fully-qualified # ❎ cross mark button + "➕", // 2795 ; fully-qualified # ➕ plus sign + "➖", // 2796 ; fully-qualified # ➖ minus sign + "➗", // 2797 ; fully-qualified # ➗ division sign + "➰", // 27B0 ; fully-qualified # ➰ curly loop + "➿", // 27BF ; fully-qualified # ➿ double curly loop + "❓", // 2753 ; fully-qualified # ❓ question mark + "❔", // 2754 ; fully-qualified # ❔ white question mark + "❕", // 2755 ; fully-qualified # ❕ white exclamation mark + "❗", // 2757 ; fully-qualified # ❗ exclamation mark + "🔟", // 1F51F ; fully-qualified # 🔟 keycap: 10 + "🔠", // 1F520 ; fully-qualified # 🔠 input latin uppercase + "🔡", // 1F521 ; fully-qualified # 🔡 input latin lowercase + "🔢", // 1F522 ; fully-qualified # 🔢 input numbers + "🔣", // 1F523 ; fully-qualified # 🔣 input symbols + "🔤", // 1F524 ; fully-qualified # 🔤 input latin letters + "🆎", // 1F18E ; fully-qualified # 🆎 AB button (blood type) + "🆑", // 1F191 ; fully-qualified # 🆑 CL button + "🆒", // 1F192 ; fully-qualified # 🆒 COOL button + "🆓", // 1F193 ; fully-qualified # 🆓 FREE button + "🆔", // 1F194 ; fully-qualified # 🆔 ID button + "🆕", // 1F195 ; fully-qualified # 🆕 NEW button + "🆖", // 1F196 ; fully-qualified # 🆖 NG button + "🆗", // 1F197 ; fully-qualified # 🆗 OK button + "🆘", // 1F198 ; fully-qualified # 🆘 SOS button + "🆙", // 1F199 ; fully-qualified # 🆙 UP! button + "🆚", // 1F19A ; fully-qualified # 🆚 VS button + "🈁", // 1F201 ; fully-qualified # 🈁 Japanese “here” button + "🈶", // 1F236 ; fully-qualified # 🈶 Japanese “not free of charge” button + "🈯", // 1F22F ; fully-qualified # 🈯 Japanese “reserved” button + "🉐", // 1F250 ; fully-qualified # 🉐 Japanese “bargain” button + "🈹", // 1F239 ; fully-qualified # 🈹 Japanese “discount” button + "🈚", // 1F21A ; fully-qualified # 🈚 Japanese “free of charge” button + "🈲", // 1F232 ; fully-qualified # 🈲 Japanese “prohibited” button + "🉑", // 1F251 ; fully-qualified # 🉑 Japanese “acceptable” button + "🈸", // 1F238 ; fully-qualified # 🈸 Japanese “application” button + "🈴", // 1F234 ; fully-qualified # 🈴 Japanese “passing grade” button + "🈳", // 1F233 ; fully-qualified # 🈳 Japanese “vacancy” button + "🈺", // 1F23A ; fully-qualified # 🈺 Japanese “open for business” button + "🈵", // 1F235 ; fully-qualified # 🈵 Japanese “no vacancy” button + "🔴", // 1F534 ; fully-qualified # 🔴 red circle + "🟠", // 1F7E0 ; fully-qualified # 🟠 orange circle + "🟡", // 1F7E1 ; fully-qualified # 🟡 yellow circle + "🟢", // 1F7E2 ; fully-qualified # 🟢 green circle + "🔵", // 1F535 ; fully-qualified # 🔵 blue circle + "🟣", // 1F7E3 ; fully-qualified # 🟣 purple circle + "🟤", // 1F7E4 ; fully-qualified # 🟤 brown circle + "⚫", // 26AB ; fully-qualified # ⚫ black circle + "⚪", // 26AA ; fully-qualified # ⚪ white circle + "🟥", // 1F7E5 ; fully-qualified # 🟥 red square + "🟧", // 1F7E7 ; fully-qualified # 🟧 orange square + "🟨", // 1F7E8 ; fully-qualified # 🟨 yellow square + "🟩", // 1F7E9 ; fully-qualified # 🟩 green square + "🟦", // 1F7E6 ; fully-qualified # 🟦 blue square + "🟪", // 1F7EA ; fully-qualified # 🟪 purple square + "🟫", // 1F7EB ; fully-qualified # 🟫 brown square + "⬛", // 2B1B ; fully-qualified # ⬛ black large square + "⬜", // 2B1C ; fully-qualified # ⬜ white large square + "◾", // 25FE ; fully-qualified # ◾ black medium-small square + "◽", // 25FD ; fully-qualified # ◽ white medium-small square + "🔶", // 1F536 ; fully-qualified # 🔶 large orange diamond + "🔷", // 1F537 ; fully-qualified # 🔷 large blue diamond + "🔸", // 1F538 ; fully-qualified # 🔸 small orange diamond + "🔹", // 1F539 ; fully-qualified # 🔹 small blue diamond + "🔺", // 1F53A ; fully-qualified # 🔺 red triangle pointed up + "🔻", // 1F53B ; fully-qualified # 🔻 red triangle pointed down + "💠", // 1F4A0 ; fully-qualified # 💠 diamond with a dot + "🔘", // 1F518 ; fully-qualified # 🔘 radio button + "🔳", // 1F533 ; fully-qualified # 🔳 white square button + "🔲", // 1F532 ; fully-qualified # 🔲 black square button + }, + { // # group: Flags + "🏁", // 1F3C1 ; fully-qualified # 🏁 chequered flag + "🚩", // 1F6A9 ; fully-qualified # 🚩 triangular flag + "🎌", // 1F38C ; fully-qualified # 🎌 crossed flags + "🏴", // 1F3F4 ; fully-qualified # 🏴 black flag + } + }; } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/util/HTMLEdit.java b/husky/app/src/main/java/com/keylesspalace/tusky/util/HTMLEdit.java index bb34425..b01e99c 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/util/HTMLEdit.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/util/HTMLEdit.java @@ -1,17 +1,15 @@ package com.keylesspalace.tusky.util; -import androidx.annotation.IntDef; -import androidx.annotation.IntRange; -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; import android.text.Editable; import android.text.Selection; -import android.text.Spannable; import android.widget.EditText; +import androidx.annotation.NonNull; import me.thanel.markdownedit.SelectionUtils; public class HTMLEdit { - private HTMLEdit() { /* cannot be instantiated */ } + private HTMLEdit() { + /* cannot be instantiated */ + } public static void addBold(@NonNull Editable text) { surroundSelectionWith(text, "<b>", "</b>"); @@ -61,7 +59,7 @@ public class HTMLEdit { public static void addLink(@NonNull EditText editText) { addLink(editText.getText()); } - + /** * Inserts a markdown code block to the specified EditText at the currently selected position. * @@ -80,7 +78,8 @@ public class HTMLEdit { addCode(editText.getText()); } - public static void surroundSelectionWith(@NonNull Editable text, @NonNull String surroundText, @NonNull String surroundText2) { + public static void surroundSelectionWith( + @NonNull Editable text, @NonNull String surroundText, @NonNull String surroundText2) { if (!SelectionUtils.hasSelection(text)) { SelectionUtils.selectWordAroundCursor(text); } @@ -91,7 +90,7 @@ public class HTMLEdit { StringBuilder result = new StringBuilder(); result.append(surroundText).append(selectedText).append(surroundText2); - + int charactersToGoBack = 0; if (selectedText.length() == 0) { charactersToGoBack = surroundText2.length(); @@ -100,5 +99,4 @@ public class HTMLEdit { SelectionUtils.replaceSelectedText(text, result); Selection.setSelection(text, selectionStart + result.length() - charactersToGoBack); } - } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/util/HttpHeaderLink.java b/husky/app/src/main/java/com/keylesspalace/tusky/util/HttpHeaderLink.java index 27f3dff..9f83cc6 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/util/HttpHeaderLink.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/util/HttpHeaderLink.java @@ -11,7 +11,6 @@ package com.keylesspalace.tusky.util; import android.net.Uri; import androidx.annotation.Nullable; - import java.util.ArrayList; import java.util.List; @@ -148,8 +147,8 @@ public class HttpHeaderLink { * @return the link matching the given relation type */ @Nullable - public static HttpHeaderLink findByRelationType(List<HttpHeaderLink> links, - String relationType) { + public static HttpHeaderLink findByRelationType( + List<HttpHeaderLink> links, String relationType) { for (HttpHeaderLink link : links) { for (Parameter parameter : link.parameters) { if (parameter.name.equals("rel") && parameter.value.equals(relationType)) { diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/util/IOUtils.java b/husky/app/src/main/java/com/keylesspalace/tusky/util/IOUtils.java index 7c3b68a..4bdc5e6 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/util/IOUtils.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/util/IOUtils.java @@ -18,7 +18,6 @@ package com.keylesspalace.tusky.util; import android.content.ContentResolver; import android.net.Uri; import androidx.annotation.Nullable; - import java.io.Closeable; import java.io.File; import java.io.FileNotFoundException; diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java b/husky/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java index c05f224..02ce107 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java @@ -19,7 +19,6 @@ import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.net.Uri; -import android.os.Build; import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.Spanned; @@ -29,26 +28,22 @@ import android.text.style.URLSpan; import android.util.Log; import android.view.View; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.browser.customtabs.CustomTabColorSchemeParams; import androidx.browser.customtabs.CustomTabsIntent; import androidx.preference.PreferenceManager; - import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.interfaces.LinkListener; - import java.net.URI; import java.net.URISyntaxException; public class LinkHelper { public static String getDomain(String urlString) { // sometimes URL can be null due to Pleroma bug - if(urlString == null) - return ""; - + if (urlString == null) return ""; + URI uri; try { uri = new URI(urlString); @@ -56,7 +51,7 @@ public class LinkHelper { return ""; } String host = uri.getHost(); - if(host == null) { + if (host == null) { return ""; } else if (host.startsWith("www.")) { return host.substring(4); @@ -74,8 +69,11 @@ public class LinkHelper { * @param mentions any '@' mentions which are known to be in the content * @param listener to notify about particular spans that are clicked */ - public static void setClickableText(TextView view, CharSequence content, - @Nullable Status.Mention[] mentions, final LinkListener listener) { + public static void setClickableText( + TextView view, + CharSequence content, + @Nullable Status.Mention[] mentions, + final LinkListener listener) { SpannableStringBuilder builder = SpannableStringBuilder.valueOf(content); URLSpan[] urlSpans = builder.getSpans(0, content.length(), URLSpan.class); for (URLSpan span : urlSpans) { @@ -87,10 +85,13 @@ public class LinkHelper { if (text.charAt(0) == '#') { final String tag = text.subSequence(1, text.length()).toString(); - customSpan = new ClickableSpanNoUnderline() { - @Override - public void onClick(@NonNull View widget) { listener.onViewTag(tag); } - }; + customSpan = + new ClickableSpanNoUnderline() { + @Override + public void onClick(@NonNull View widget) { + listener.onViewTag(tag); + } + }; } else if (text.charAt(0) == '@' && mentions != null && mentions.length > 0) { String accountUsername = text.subSequence(1, text.length()).toString(); /* There may be multiple matches for users on different instances with the same @@ -108,20 +109,24 @@ public class LinkHelper { } if (id != null) { final String accountId = id; - customSpan = new ClickableSpanNoUnderline() { - @Override - public void onClick(@NonNull View widget) { listener.onViewAccount(accountId); } - }; + customSpan = + new ClickableSpanNoUnderline() { + @Override + public void onClick(@NonNull View widget) { + listener.onViewAccount(accountId); + } + }; } } if (customSpan == null) { - customSpan = new CustomURLSpan(span.getURL()) { - @Override - public void onClick(View widget) { - listener.onViewUrl(getURL()); - } - }; + customSpan = + new CustomURLSpan(span.getURL()) { + @Override + public void onClick(View widget) { + listener.onViewUrl(getURL()); + } + }; } builder.removeSpan(span); builder.setSpan(customSpan, start, end, flags); @@ -129,8 +134,8 @@ public class LinkHelper { /* Add zero-width space after links in end of line to fix its too large hitbox. * See also : https://github.com/tuskyapp/Tusky/issues/846 * https://github.com/tuskyapp/Tusky/pull/916 */ - if (end >= builder.length() || - builder.subSequence(end, end + 1).toString().equals("\n")){ + if (end >= builder.length() + || builder.subSequence(end, end + 1).toString().equals("\n")) { builder.insert(end, "\u200B"); } } @@ -161,10 +166,13 @@ public class LinkHelper { for (Status.Mention mention : mentions) { String accountUsername = mention.getLocalUsername(); final String accountId = mention.getId(); - ClickableSpan customSpan = new ClickableSpanNoUnderline() { - @Override - public void onClick(@NonNull View widget) { listener.onViewAccount(accountId); } - }; + ClickableSpan customSpan = + new ClickableSpanNoUnderline() { + @Override + public void onClick(@NonNull View widget) { + listener.onViewAccount(accountId); + } + }; end += 1 + accountUsername.length(); // length of @ + username flags = builder.getSpanFlags(customSpan); @@ -201,13 +209,13 @@ public class LinkHelper { * @param context context */ public static void openLink(String url, Context context) { - if(url == null) - return; - + if (url == null) return; + Uri uri = Uri.parse(url).normalizeScheme(); - boolean useCustomTabs = PreferenceManager.getDefaultSharedPreferences(context) - .getBoolean("customTabs", false); + boolean useCustomTabs = + PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean("customTabs", false); if (useCustomTabs) { openLinkInCustomTab(uri, context); } else { @@ -231,8 +239,7 @@ public class LinkHelper { } /** - * tries to open a link in a custom tab - * falls back to browser if not possible + * tries to open a link in a custom tab falls back to browser if not possible * * @param uri the uri to open * @param context context @@ -242,16 +249,18 @@ public class LinkHelper { int navigationbarColor = ThemeUtils.getColor(context, android.R.attr.navigationBarColor); int navigationbarDividerColor = ThemeUtils.getColor(context, R.attr.dividerColor); - CustomTabColorSchemeParams colorSchemeParams = new CustomTabColorSchemeParams.Builder() - .setToolbarColor(toolbarColor) - .setNavigationBarColor(navigationbarColor) - .setNavigationBarDividerColor(navigationbarDividerColor) - .build(); + CustomTabColorSchemeParams colorSchemeParams = + new CustomTabColorSchemeParams.Builder() + .setToolbarColor(toolbarColor) + .setNavigationBarColor(navigationbarColor) + .setNavigationBarDividerColor(navigationbarDividerColor) + .build(); - CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder() - .setDefaultColorSchemeParams(colorSchemeParams) - .setShowTitle(true) - .build(); + CustomTabsIntent customTabsIntent = + new CustomTabsIntent.Builder() + .setDefaultColorSchemeParams(colorSchemeParams) + .setShowTitle(true) + .build(); try { customTabsIntent.launchUrl(context, uri); @@ -259,7 +268,5 @@ public class LinkHelper { Log.w("LinkHelper", "Activity was not found for intent " + customTabsIntent); openLinkInBrowser(uri, context); } - } - } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/util/PagingRequestHelper.java b/husky/app/src/main/java/com/keylesspalace/tusky/util/PagingRequestHelper.java index 4f7d3ef..389bce4 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/util/PagingRequestHelper.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/util/PagingRequestHelper.java @@ -24,17 +24,19 @@ import java.util.Arrays; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; + /** * A helper class for {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}s and * {@link androidx.paging.DataSource}s to help with tracking network requests. - * <p> - * It is designed to support 3 types of requests, {@link RequestType#INITIAL INITIAL}, - * {@link RequestType#BEFORE BEFORE} and {@link RequestType#AFTER AFTER} and runs only 1 request - * for each of them via {@link #runIfNotRunning(RequestType, Request)}. - * <p> - * It tracks a {@link Status} and an {@code error} for each {@link RequestType}. - * <p> - * A sample usage of this class to limit requests looks like this: + * + * <p>It is designed to support 3 types of requests, {@link RequestType#INITIAL INITIAL}, {@link + * RequestType#BEFORE BEFORE} and {@link RequestType#AFTER AFTER} and runs only 1 request for each + * of them via {@link #runIfNotRunning(RequestType, Request)}. + * + * <p>It tracks a {@link Status} and an {@code error} for each {@link RequestType}. + * + * <p>A sample usage of this class to limit requests looks like this: + * * <pre> * class PagingBoundaryCallback extends PagedList.BoundaryCallback<MyItem> { * // TODO replace with an executor from your application @@ -82,9 +84,10 @@ import java.util.concurrent.atomic.AtomicBoolean; * } * } * </pre> - * <p> - * The helper provides an API to observe combined request status, which can be reported back to the - * application based on your business rules. + * + * <p>The helper provides an API to observe combined request status, which can be reported back to + * the application based on your business rules. + * * <pre> * MutableLiveData<PagingRequestHelper.Status> combined = new MutableLiveData<>(); * helper.addListener(status -> { @@ -106,16 +109,19 @@ import java.util.concurrent.atomic.AtomicBoolean; public class PagingRequestHelper { private final Object mLock = new Object(); private final Executor mRetryService; + @GuardedBy("mLock") - private final RequestQueue[] mRequestQueues = new RequestQueue[] - {new RequestQueue(RequestType.INITIAL), - new RequestQueue(RequestType.BEFORE), - new RequestQueue(RequestType.AFTER)}; - @NonNull - final CopyOnWriteArrayList<Listener> mListeners = new CopyOnWriteArrayList<>(); + private final RequestQueue[] mRequestQueues = + new RequestQueue[] { + new RequestQueue(RequestType.INITIAL), + new RequestQueue(RequestType.BEFORE), + new RequestQueue(RequestType.AFTER) + }; + + @NonNull final CopyOnWriteArrayList<Listener> mListeners = new CopyOnWriteArrayList<>(); /** - * Creates a new PagingRequestHelper with the given {@link Executor} which is used to run - * retry actions. + * Creates a new PagingRequestHelper with the given {@link Executor} which is used to run retry + * actions. * * @param retryService The {@link Executor} that can run the retry actions. */ @@ -144,10 +150,10 @@ public class PagingRequestHelper { /** * Runs the given {@link Request} if no other requests in the given request type is already * running. - * <p> - * If run, the request will be run in the current thread. * - * @param type The type of the request. + * <p>If run, the request will be run in the current thread. + * + * @param type The type of the request. * @param request The request to run. * @return True if the request is run, false otherwise. */ @@ -176,24 +182,27 @@ public class PagingRequestHelper { wrapper.run(); return true; } + @GuardedBy("mLock") private StatusReport prepareStatusReportLocked() { - Throwable[] errors = new Throwable[]{ - mRequestQueues[0].mLastError, - mRequestQueues[1].mLastError, - mRequestQueues[2].mLastError - }; + Throwable[] errors = + new Throwable[] { + mRequestQueues[0].mLastError, + mRequestQueues[1].mLastError, + mRequestQueues[2].mLastError + }; return new StatusReport( getStatusForLocked(RequestType.INITIAL), getStatusForLocked(RequestType.BEFORE), getStatusForLocked(RequestType.AFTER), - errors - ); + errors); } + @GuardedBy("mLock") private Status getStatusForLocked(RequestType type) { return mRequestQueues[type.ordinal()].mStatus; } + @AnyThread @VisibleForTesting void recordResult(@NonNull RequestWrapper wrapper, @Nullable Throwable throwable) { @@ -219,6 +228,7 @@ public class PagingRequestHelper { dispatchReport(report); } } + private void dispatchReport(StatusReport report) { for (Listener listener : mListeners) { listener.onStatusChange(report); @@ -246,39 +256,42 @@ public class PagingRequestHelper { } return retried; } + static class RequestWrapper implements Runnable { - @NonNull - final Request mRequest; - @NonNull - final PagingRequestHelper mHelper; - @NonNull - final RequestType mType; - RequestWrapper(@NonNull Request request, @NonNull PagingRequestHelper helper, + @NonNull final Request mRequest; + @NonNull final PagingRequestHelper mHelper; + @NonNull final RequestType mType; + + RequestWrapper( + @NonNull Request request, + @NonNull PagingRequestHelper helper, @NonNull RequestType type) { mRequest = request; mHelper = helper; mType = type; } + @Override public void run() { mRequest.run(new Request.Callback(this, mHelper)); } + void retry(Executor service) { - service.execute(new Runnable() { - @Override - public void run() { - mHelper.runIfNotRunning(mType, mRequest); - } - }); + service.execute( + new Runnable() { + @Override + public void run() { + mHelper.runIfNotRunning(mType, mRequest); + } + }); } } /** * Runner class that runs a request tracked by the {@link PagingRequestHelper}. - * <p> - * When a request is invoked, it must call one of {@link Callback#recordFailure(Throwable)} - * or {@link Callback#recordSuccess()} once and only once. This call - * can be made any time. Until that method call is made, {@link PagingRequestHelper} will - * consider the request is running. + * + * <p>When a request is invoked, it must call one of {@link Callback#recordFailure(Throwable)} + * or {@link Callback#recordSuccess()} once and only once. This call can be made any time. Until + * that method call is made, {@link PagingRequestHelper} will consider the request is running. */ @FunctionalInterface public interface Request { @@ -289,20 +302,17 @@ public class PagingRequestHelper { * @param callback The callback that should be invoked with the result. */ void run(Callback callback); - /** - * Callback class provided to the {@link #run(Callback)} method to report the result. - */ + /** Callback class provided to the {@link #run(Callback)} method to report the result. */ class Callback { private final AtomicBoolean mCalled = new AtomicBoolean(); private final RequestWrapper mWrapper; private final PagingRequestHelper mHelper; + Callback(RequestWrapper wrapper, PagingRequestHelper helper) { mWrapper = wrapper; mHelper = helper; } - /** - * Call this method when the request succeeds and new data is fetched. - */ + /** Call this method when the request succeeds and new data is fetched. */ @SuppressWarnings("unused") public final void recordSuccess() { if (mCalled.compareAndSet(false, true)) { @@ -313,8 +323,8 @@ public class PagingRequestHelper { } } /** - * Call this method with the failure message and the request can be retried via - * {@link #retryAllFailed()}. + * Call this method with the failure message and the request can be retried via {@link + * #retryAllFailed()}. * * @param throwable The error that occured while carrying out the request. */ @@ -322,8 +332,9 @@ public class PagingRequestHelper { public final void recordFailure(@NonNull Throwable throwable) { //noinspection ConstantConditions if (throwable == null) { - throw new IllegalArgumentException("You must provide a throwable describing" - + " the error to record the failure"); + throw new IllegalArgumentException( + "You must provide a throwable describing" + + " the error to record the failure"); } if (mCalled.compareAndSet(false, true)) { mHelper.recordResult(mWrapper, throwable); @@ -335,28 +346,23 @@ public class PagingRequestHelper { } } /** - * Data class that holds the information about the current status of the ongoing requests - * using this helper. + * Data class that holds the information about the current status of the ongoing requests using + * this helper. */ public static final class StatusReport { - /** - * Status of the latest request that were submitted with {@link RequestType#INITIAL}. - */ - @NonNull - public final Status initial; - /** - * Status of the latest request that were submitted with {@link RequestType#BEFORE}. - */ - @NonNull - public final Status before; - /** - * Status of the latest request that were submitted with {@link RequestType#AFTER}. - */ - @NonNull - public final Status after; - @NonNull - private final Throwable[] mErrors; - StatusReport(@NonNull Status initial, @NonNull Status before, @NonNull Status after, + /** Status of the latest request that were submitted with {@link RequestType#INITIAL}. */ + @NonNull public final Status initial; + /** Status of the latest request that were submitted with {@link RequestType#BEFORE}. */ + @NonNull public final Status before; + /** Status of the latest request that were submitted with {@link RequestType#AFTER}. */ + @NonNull public final Status after; + + @NonNull private final Throwable[] mErrors; + + StatusReport( + @NonNull Status initial, + @NonNull Status before, + @NonNull Status after, @NonNull Throwable[] errors) { this.initial = initial; this.before = before; @@ -369,9 +375,7 @@ public class PagingRequestHelper { * @return True if there are any running requests, false otherwise. */ public boolean hasRunning() { - return initial == Status.RUNNING - || before == Status.RUNNING - || after == Status.RUNNING; + return initial == Status.RUNNING || before == Status.RUNNING || after == Status.RUNNING; } /** * Convenience method to check if there are any requests that resulted in an error. @@ -379,30 +383,34 @@ public class PagingRequestHelper { * @return True if there are any requests that finished with error, false otherwise. */ public boolean hasError() { - return initial == Status.FAILED - || before == Status.FAILED - || after == Status.FAILED; + return initial == Status.FAILED || before == Status.FAILED || after == Status.FAILED; } /** * Returns the error for the given request type. * * @param type The request type for which the error should be returned. * @return The {@link Throwable} returned by the failing request with the given type or - * {@code null} if the request for the given type did not fail. + * {@code null} if the request for the given type did not fail. */ @Nullable public Throwable getErrorFor(@NonNull RequestType type) { return mErrors[type.ordinal()]; } + @Override public String toString() { return "StatusReport{" - + "initial=" + initial - + ", before=" + before - + ", after=" + after - + ", mErrors=" + Arrays.toString(mErrors) + + "initial=" + + initial + + ", before=" + + before + + ", after=" + + after + + ", mErrors=" + + Arrays.toString(mErrors) + '}'; } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -414,6 +422,7 @@ public class PagingRequestHelper { // Probably incorrect - comparing Object[] arrays with Arrays.equals return Arrays.equals(mErrors, that.mErrors); } + @Override public int hashCode() { int result = initial.hashCode(); @@ -423,9 +432,7 @@ public class PagingRequestHelper { return result; } } - /** - * Listener interface to get notified by request status changes. - */ + /** Listener interface to get notified by request status changes. */ public interface Listener { /** * Called when the status for any of the requests has changed. @@ -434,58 +441,45 @@ public class PagingRequestHelper { */ void onStatusChange(@NonNull StatusReport report); } - /** - * Represents the status of a Request for each {@link RequestType}. - */ + /** Represents the status of a Request for each {@link RequestType}. */ public enum Status { - /** - * There is current a running request. - */ + /** There is current a running request. */ RUNNING, - /** - * The last request has succeeded or no such requests have ever been run. - */ + /** The last request has succeeded or no such requests have ever been run. */ SUCCESS, - /** - * The last request has failed. - */ + /** The last request has failed. */ FAILED } - /** - * Available request types. - */ + /** Available request types. */ public enum RequestType { /** - * Corresponds to an initial request made to a {@link androidx.paging.DataSource} or the empty state for - * a {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}. + * Corresponds to an initial request made to a {@link androidx.paging.DataSource} or the + * empty state for a {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}. */ INITIAL, /** * Corresponds to the {@code loadBefore} calls in {@link androidx.paging.DataSource} or - * {@code onItemAtFrontLoaded} in - * {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}. + * {@code onItemAtFrontLoaded} in {@link androidx.paging.PagedList.BoundaryCallback + * BoundaryCallback}. */ BEFORE, /** * Corresponds to the {@code loadAfter} calls in {@link androidx.paging.DataSource} or - * {@code onItemAtEndLoaded} in - * {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}. + * {@code onItemAtEndLoaded} in {@link androidx.paging.PagedList.BoundaryCallback + * BoundaryCallback}. */ AFTER } + class RequestQueue { - @NonNull - final RequestType mRequestType; - @Nullable - RequestWrapper mFailed; - @Nullable - Request mRunning; - @Nullable - Throwable mLastError; - @NonNull - Status mStatus = Status.SUCCESS; + @NonNull final RequestType mRequestType; + @Nullable RequestWrapper mFailed; + @Nullable Request mRunning; + @Nullable Throwable mLastError; + @NonNull Status mStatus = Status.SUCCESS; + RequestQueue(@NonNull RequestType requestType) { mRequestType = requestType; } } -}
\ No newline at end of file +} diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/util/PairedList.java b/husky/app/src/main/java/com/keylesspalace/tusky/util/PairedList.java index a0880a5..230b1cb 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/util/PairedList.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/util/PairedList.java @@ -2,24 +2,22 @@ package com.keylesspalace.tusky.util; import androidx.annotation.Nullable; import androidx.arch.core.util.Function; - import java.util.AbstractList; import java.util.ArrayList; import java.util.List; - /** * This list implementation can help to keep two lists in sync - like real models and view models. * Every operation on the main list triggers update of the supplementary list (but not vice versa). - * This makes sure that the main list is always the source of truth. - * Main list is projected to the supplementary list by the passed mapper function. - * Paired list is newer actually exposed and clients are provided with {@code getPairedCopy()}, - * {@code getPairedItem()} and {@code setPairedItem()}. This prevents modifications of the - * supplementary list size so lists are always have the same length. - * This implementation will not try to recover from exceptional cases so lists may be out of sync - * after the exception. + * This makes sure that the main list is always the source of truth. Main list is projected to the + * supplementary list by the passed mapper function. Paired list is newer actually exposed and + * clients are provided with {@code getPairedCopy()}, {@code getPairedItem()} and {@code + * setPairedItem()}. This prevents modifications of the supplementary list size so lists are always + * have the same length. This implementation will not try to recover from exceptional cases so lists + * may be out of sync after the exception. + * + * <p>It is most useful with immutable data because we cannot track changes inside stored objects. * - * It is most useful with immutable data because we cannot track changes inside stored objects. * @param <T> type of elements in the main list * @param <V> type of elements in supplementary list */ @@ -30,8 +28,9 @@ public final class PairedList<T, V> extends AbstractList<T> { /** * Construct new paired list. Main and supplementary lists will be empty. + * * @param mapper Function, which will be used to translate items from the main list to the - * supplementary one. + * supplementary one. */ public PairedList(Function<T, ? extends V> mapper) { this.mapper = mapper; diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/util/SaveTootHelper.java b/husky/app/src/main/java/com/keylesspalace/tusky/util/SaveTootHelper.java index 0d30b14..125d539 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/util/SaveTootHelper.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/util/SaveTootHelper.java @@ -1,5 +1,7 @@ package com.keylesspalace.tusky.util; +import static org.koin.java.KoinJavaComponent.inject; + import android.content.Context; import android.net.Uri; import android.util.Log; @@ -11,7 +13,6 @@ import com.keylesspalace.tusky.db.TootDao; import com.keylesspalace.tusky.db.TootEntity; import java.util.ArrayList; import kotlin.Lazy; -import static org.koin.java.KoinJavaComponent.inject; public final class SaveTootHelper { @@ -28,7 +29,7 @@ public final class SaveTootHelper { public void deleteDraft(int tootId) { TootEntity item = tootDao.find(tootId); - if(item != null) { + if (item != null) { deleteDraft(item); } } @@ -36,12 +37,12 @@ public final class SaveTootHelper { public void deleteDraft(@NonNull TootEntity item) { // Delete any media files associated with the status. ArrayList<String> uris = - gson.getValue().fromJson(item.getUrls(), new TypeToken<ArrayList<String>>() { - }.getType()); - if(uris != null) { - for(String uriString : uris) { + gson.getValue() + .fromJson(item.getUrls(), new TypeToken<ArrayList<String>>() {}.getType()); + if (uris != null) { + for (String uriString : uris) { Uri uri = Uri.parse(uriString); - if(context.getContentResolver().delete(uri, null, null) == 0) { + if (context.getContentResolver().delete(uri, null, null) == 0) { Log.e(TAG, String.format("Did not delete file %s.", uriString)); } } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.java b/husky/app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.java index 8c04a7d..f8100eb 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.java @@ -21,7 +21,6 @@ import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.util.TypedValue; - import androidx.annotation.AttrRes; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; @@ -52,7 +51,7 @@ public class ThemeUtils { } public static int getDimension(@NonNull Context context, @AttrRes int attribute) { - TypedArray array = context.obtainStyledAttributes(new int[] { attribute }); + TypedArray array = context.obtainStyledAttributes(new int[] {attribute}); int dimen = array.getDimensionPixelSize(0, -1); array.recycle(); return dimen; diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/util/TimestampUtils.java b/husky/app/src/main/java/com/keylesspalace/tusky/util/TimestampUtils.java index c94b422..ce07f1a 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/util/TimestampUtils.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/util/TimestampUtils.java @@ -16,7 +16,6 @@ package com.keylesspalace.tusky.util; import android.content.Context; - import com.keylesspalace.tusky.R; public class TimestampUtils { @@ -101,5 +100,4 @@ public class TimestampUtils { } return context.getResources().getQuantityString(format, (int) span, (int) span); } - } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/util/VersionUtils.java b/husky/app/src/main/java/com/keylesspalace/tusky/util/VersionUtils.java index ef4801a..f57d286 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/util/VersionUtils.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/util/VersionUtils.java @@ -16,7 +16,6 @@ package com.keylesspalace.tusky.util; import androidx.annotation.NonNull; - import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -40,7 +39,7 @@ public class VersionUtils { } public boolean supportsScheduledToots() { - return (major == 2) ? ( (minor == 7) ? (patch >= 0) : (minor > 7) ) : (major > 2); + return (major == 2) ? ((minor == 7) ? (patch >= 0) : (minor > 7)) : (major > 2); } public boolean isPleroma() { diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java b/husky/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java index 95c630b..fd3c37d 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java @@ -22,24 +22,23 @@ package com.keylesspalace.tusky.util; import androidx.annotation.NonNull; import androidx.annotation.Nullable; - -import com.keylesspalace.tusky.entity.Notification; -import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.Chat; import com.keylesspalace.tusky.entity.ChatMessage; +import com.keylesspalace.tusky.entity.Notification; +import com.keylesspalace.tusky.entity.Status; +import com.keylesspalace.tusky.viewdata.ChatMessageViewData; +import com.keylesspalace.tusky.viewdata.ChatViewData; import com.keylesspalace.tusky.viewdata.NotificationViewData; import com.keylesspalace.tusky.viewdata.StatusViewData; -import com.keylesspalace.tusky.viewdata.ChatViewData; -import com.keylesspalace.tusky.viewdata.ChatMessageViewData; public final class ViewDataUtils { @Nullable - public static StatusViewData.Concrete statusToViewData(@Nullable Status status, - boolean alwaysShowSensitiveMedia, - boolean alwaysOpenSpoiler) { + public static StatusViewData.Concrete statusToViewData( + @Nullable Status status, boolean alwaysShowSensitiveMedia, boolean alwaysOpenSpoiler) { if (status == null) return null; Status visibleStatus = status.getReblog() == null ? status : status.getReblog(); - return new StatusViewData.Builder().setId(status.getId()) + return new StatusViewData.Builder() + .setId(status.getId()) .setAttachments(visibleStatus.getAttachments()) .setAvatar(visibleStatus.getAccount().getAvatar()) .setContent(visibleStatus.getContent()) @@ -56,11 +55,14 @@ public final class ViewDataUtils { .setIsShowingSensitiveContent(false) .setMentions(visibleStatus.getMentions()) .setNickname(visibleStatus.getAccount().getUsername()) - .setRebloggedAvatar(status.getReblog() == null ? null : status.getAccount().getAvatar()) + .setRebloggedAvatar( + status.getReblog() == null ? null : status.getAccount().getAvatar()) .setSensitive(visibleStatus.getSensitive()) - .setIsShowingSensitiveContent(alwaysShowSensitiveMedia || !visibleStatus.getSensitive()) + .setIsShowingSensitiveContent( + alwaysShowSensitiveMedia || !visibleStatus.getSensitive()) .setSpoilerText(visibleStatus.getSpoilerText()) - .setRebloggedByUsername(status.getReblog() == null ? null : status.getAccount().getDisplayName()) + .setRebloggedByUsername( + status.getReblog() == null ? null : status.getAccount().getDisplayName()) .setUserFullName(visibleStatus.getAccount().getName()) .setVisibility(visibleStatus.getVisibility()) .setSenderId(visibleStatus.getAccount().getId()) @@ -68,8 +70,10 @@ public final class ViewDataUtils { .setApplication(visibleStatus.getApplication()) .setStatusEmojis(visibleStatus.getEmojis()) .setAccountEmojis(visibleStatus.getAccount().getEmojis()) - .setRebloggedByEmojis(status.getReblog() == null ? null : status.getAccount().getEmojis()) - .setCollapsible(SmartLengthInputFilterKt.shouldTrimStatus(visibleStatus.getContent())) + .setRebloggedByEmojis( + status.getReblog() == null ? null : status.getAccount().getEmojis()) + .setCollapsible( + SmartLengthInputFilterKt.shouldTrimStatus(visibleStatus.getContent())) .setCollapsed(true) .setPoll(visibleStatus.getPoll()) .setCard(visibleStatus.getCard()) @@ -83,26 +87,21 @@ public final class ViewDataUtils { .createStatusViewData(); } - public static NotificationViewData.Concrete notificationToViewData(Notification notification, - boolean alwaysShowSensitiveData, - boolean alwaysOpenSpoiler) { + public static NotificationViewData.Concrete notificationToViewData( + Notification notification, boolean alwaysShowSensitiveData, boolean alwaysOpenSpoiler) { return new NotificationViewData.Concrete( notification.getType(), notification.getId(), notification.getAccount(), statusToViewData( - notification.getStatus(), - alwaysShowSensitiveData, - alwaysOpenSpoiler - ), + notification.getStatus(), alwaysShowSensitiveData, alwaysOpenSpoiler), notification.getEmoji(), notification.getEmojiUrl(), - notification.getTarget() - ); + notification.getTarget()); } public static ChatMessageViewData.Concrete chatMessageToViewData(@Nullable ChatMessage msg) { - if(msg == null) return null; + if (msg == null) return null; return new ChatMessageViewData.Concrete( msg.getId(), @@ -112,8 +111,7 @@ public final class ViewDataUtils { msg.getCreatedAt(), msg.getAttachment(), msg.getEmojis(), - msg.getCard() - ); + msg.getCard()); } @NonNull @@ -122,10 +120,7 @@ public final class ViewDataUtils { chat.getAccount(), chat.getId(), chat.getUnread(), - chatMessageToViewData( - chat.getLastMessage() - ), - chat.getUpdatedAt() - ); + chatMessageToViewData(chat.getLastMessage()), + chat.getUpdatedAt()); } } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/util/ViewPager2Fix.java b/husky/app/src/main/java/com/keylesspalace/tusky/util/ViewPager2Fix.java index 4698f44..3e92e13 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/util/ViewPager2Fix.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/util/ViewPager2Fix.java @@ -1,40 +1,35 @@ package com.keylesspalace.tusky.util; -import androidx.viewpager2.widget.ViewPager2; import androidx.recyclerview.widget.RecyclerView; +import androidx.viewpager2.widget.ViewPager2; import java.lang.reflect.*; -import java.lang.*; -/** - * ViewPager2 written by monkeys! - */ +/** ViewPager2 written by monkeys! */ public class ViewPager2Fix { - /** - * Thanks to @al.e.shevelev@medium.com for solution - */ - public static Field getViewPagerRecyclerViewField() throws NoSuchFieldException { - Field f = ViewPager2.class.getDeclaredField("mRecyclerView"); - f.setAccessible(true); - return f; - } - - public static Field getRecyclerViewTouchSlopField() throws NoSuchFieldException { - Field f = RecyclerView.class.getDeclaredField("mTouchSlop"); - f.setAccessible(true); - return f; - } - - public static void reduceVelocity(ViewPager2 pager, float val) { - try { - Field recyclerViewField = getViewPagerRecyclerViewField(); - Field touchSlopField = getRecyclerViewTouchSlopField(); + /** Thanks to @al.e.shevelev@medium.com for solution */ + public static Field getViewPagerRecyclerViewField() throws NoSuchFieldException { + Field f = ViewPager2.class.getDeclaredField("mRecyclerView"); + f.setAccessible(true); + return f; + } + + public static Field getRecyclerViewTouchSlopField() throws NoSuchFieldException { + Field f = RecyclerView.class.getDeclaredField("mTouchSlop"); + f.setAccessible(true); + return f; + } + + public static void reduceVelocity(ViewPager2 pager, float val) { + try { + Field recyclerViewField = getViewPagerRecyclerViewField(); + Field touchSlopField = getRecyclerViewTouchSlopField(); - RecyclerView recyclerView = (RecyclerView)recyclerViewField.get(pager); - int touchSlop = (int)touchSlopField.get(recyclerView); - touchSlopField.setInt(recyclerView, (int)(touchSlop*val)); - } catch(Exception e) { - // all possible exceptions must be caught during tests - ; - } - } + RecyclerView recyclerView = (RecyclerView) recyclerViewField.get(pager); + int touchSlop = (int) touchSlopField.get(recyclerView); + touchSlopField.setInt(recyclerView, (int) (touchSlop * val)); + } catch (Exception e) { + // all possible exceptions must be caught during tests + ; + } + } } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/view/BezelImageView.java b/husky/app/src/main/java/com/keylesspalace/tusky/view/BezelImageView.java index c31b37e..2a57a8e 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/view/BezelImageView.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/view/BezelImageView.java @@ -21,10 +21,7 @@ import android.util.AttributeSet; import android.view.View; import android.view.ViewOutlineProvider; -/** - * override BezelImageView from MaterialDrawer library to provide custom outline - */ - +/** override BezelImageView from MaterialDrawer library to provide custom outline */ public class BezelImageView extends com.mikepenz.materialdrawer.view.BezelImageView { public BezelImageView(Context context) { this(context, null); diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/view/EmojiKeyboard.java b/husky/app/src/main/java/com/keylesspalace/tusky/view/EmojiKeyboard.java index 501e02c..358a887 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/view/EmojiKeyboard.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/view/EmojiKeyboard.java @@ -1,23 +1,21 @@ package com.keylesspalace.tusky.view; -import android.view.*; +import android.app.*; import android.content.*; +import android.text.*; import android.util.*; +import android.view.*; import android.widget.*; -import android.app.*; -import android.text.*; -import com.google.android.material.tabs.TabLayout; -import com.google.android.material.tabs.TabLayoutMediator; - import androidx.annotation.NonNull; -import androidx.viewpager2.widget.ViewPager2; -import androidx.recyclerview.widget.RecyclerView; import androidx.preference.PreferenceManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.viewpager2.widget.ViewPager2; +import com.google.android.material.tabs.TabLayout; +import com.google.android.material.tabs.TabLayoutMediator; import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.adapter.StickerAdapter; import com.keylesspalace.tusky.adapter.UnicodeEmojiAdapter; import com.keylesspalace.tusky.entity.StickerPack; - import java.util.*; public class EmojiKeyboard extends LinearLayout { @@ -49,14 +47,14 @@ public class EmojiKeyboard extends LinearLayout { void init(Context context) { inflate(context, R.layout.item_emoji_picker, this); - + pref = PreferenceManager.getDefaultSharedPreferences(context); tabs = findViewById(R.id.picker_tabs); pager = findViewById(R.id.picker_pager); } - + public static final int UNICODE_MODE = 0; - public static final int CUSTOM_MODE = 1; + public static final int CUSTOM_MODE = 1; public static final int STICKER_MODE = 2; private void setupKeyboardWithAdapter(RecyclerView.Adapter adapter, String preferenceKey) { @@ -65,89 +63,100 @@ public class EmojiKeyboard extends LinearLayout { List<String> list = Arrays.asList(pref.getString(preferenceKey, "").split(RECENTS_DELIM)); recents = new LinkedHashSet<String>(list); - ((EmojiKeyboardAdapter)adapter).onRecentsUpdate(recents); + ((EmojiKeyboardAdapter) adapter).onRecentsUpdate(recents); pager.setAdapter(adapter); - if(currentMediator != null) - currentMediator.detach(); + if (currentMediator != null) currentMediator.detach(); - currentMediator = new TabLayoutMediator(tabs, pager, (TabLayoutMediator.TabConfigurationStrategy)adapter); + currentMediator = + new TabLayoutMediator( + tabs, pager, (TabLayoutMediator.TabConfigurationStrategy) adapter); currentMediator.attach(); } public void setupStickerKeyboard(OnEmojiSelectedListener listener, StickerPack packs[]) { MAX_RECENTS_ITEMS = 20; - setupKeyboardWithAdapter(new StickerAdapter(packs, (_id, _emoji) -> { - this.appendToRecents(_emoji); - listener.onEmojiSelected(_id, _emoji); - }), "STICKER_RECENTS"); + setupKeyboardWithAdapter( + new StickerAdapter( + packs, + (_id, _emoji) -> { + this.appendToRecents(_emoji); + listener.onEmojiSelected(_id, _emoji); + }), + "STICKER_RECENTS"); } public void setupKeyboard(String id, int mode, OnEmojiSelectedListener listener) { - switch(mode) { - // WOOOPS, I forgot that I need to pass data to adapter - // For stickers, use SetupStickerKeyboard instead - // For custom emoji, use TODO + switch (mode) { + // WOOOPS, I forgot that I need to pass data to adapter + // For stickers, use SetupStickerKeyboard instead + // For custom emoji, use TODO case CUSTOM_MODE: case STICKER_MODE: throw new IllegalArgumentException(); default: case UNICODE_MODE: - setupKeyboardWithAdapter(new UnicodeEmojiAdapter(id, (_id, _emoji) -> { - this.appendToRecents(_emoji); - listener.onEmojiSelected(_id, _emoji); - }), "UNICODE_RECENTS"); + setupKeyboardWithAdapter( + new UnicodeEmojiAdapter( + id, + (_id, _emoji) -> { + this.appendToRecents(_emoji); + listener.onEmojiSelected(_id, _emoji); + }), + "UNICODE_RECENTS"); } } - + private void appendToRecents(String id) { recents.remove(id); recents.add(id); int size = recents.size(); String joined; final SharedPreferences.Editor editor = pref.edit(); - if(size > MAX_RECENTS_ITEMS) { + if (size > MAX_RECENTS_ITEMS) { List<String> list = new ArrayList<String>(recents); list = list.subList(size - MAX_RECENTS_ITEMS, size); joined = TextUtils.join(RECENTS_DELIM, list); - if(isSticky) { + if (isSticky) { recents = new LinkedHashSet<String>(list); } } else { joined = TextUtils.join(RECENTS_DELIM, recents); } - + editor.putString(preferenceKey, joined); editor.apply(); - + // no point on updating view if we are will be closed - if(isSticky) { - ((EmojiKeyboardAdapter)adapter).onRecentsUpdate(recents); + if (isSticky) { + ((EmojiKeyboardAdapter) adapter).onRecentsUpdate(recents); } } public interface OnEmojiSelectedListener { void onEmojiSelected(@NonNull String id, @NonNull String emoji); } - + public interface EmojiKeyboardAdapter { void onRecentsUpdate(@NonNull Set<String> set); } - + public static void show(Context ctx, String id, int mode, OnEmojiSelectedListener listener) { final Dialog dialog = new Dialog(ctx); - + dialog.setTitle(null); dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); dialog.setContentView(R.layout.dialog_emoji_keyboard); - EmojiKeyboard kbd = (EmojiKeyboard)dialog.findViewById(R.id.dialog_emoji_keyboard); - kbd.setupKeyboard(id, mode, (_id, _emoji) -> { - listener.onEmojiSelected(_id, _emoji); - if(!kbd.isSticky) - dialog.dismiss(); - }); - + EmojiKeyboard kbd = (EmojiKeyboard) dialog.findViewById(R.id.dialog_emoji_keyboard); + kbd.setupKeyboard( + id, + mode, + (_id, _emoji) -> { + listener.onEmojiSelected(_id, _emoji); + if (!kbd.isSticky) dialog.dismiss(); + }); + dialog.show(); } } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/view/EndlessOnScrollListener.java b/husky/app/src/main/java/com/keylesspalace/tusky/view/EndlessOnScrollListener.java index 50f9ea6..31f039c 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/view/EndlessOnScrollListener.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/view/EndlessOnScrollListener.java @@ -35,7 +35,6 @@ public abstract class EndlessOnScrollListener extends RecyclerView.OnScrollListe int lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition(); if (totalItemCount < previousTotalItemCount) { previousTotalItemCount = totalItemCount; - } if (totalItemCount != previousTotalItemCount) { previousTotalItemCount = totalItemCount; diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java b/husky/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java index 485d7f7..aa4466c 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java @@ -22,23 +22,19 @@ package com.keylesspalace.tusky.viewdata; import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.Notification; - -import java.util.Objects; - import io.reactivex.annotations.Nullable; +import java.util.Objects; /** - * Class to represent data required to display either a notification or a placeholder. - * It is either a {@link Placeholder} or a {@link Concrete}. - * It is modelled this way because close relationship between placeholder and concrete notification - * is fine in this case. Placeholder case is not modelled as a type of notification because - * invariants would be violated and because it would model domain incorrectly. It is prefereable to - * {@link com.keylesspalace.tusky.util.Either} because class hierarchy is cheaper, faster and - * more native. + * Class to represent data required to display either a notification or a placeholder. It is either + * a {@link Placeholder} or a {@link Concrete}. It is modelled this way because close relationship + * between placeholder and concrete notification is fine in this case. Placeholder case is not + * modelled as a type of notification because invariants would be violated and because it would + * model domain incorrectly. It is prefereable to {@link com.keylesspalace.tusky.util.Either} + * because class hierarchy is cheaper, faster and more native. */ public abstract class NotificationViewData { - private NotificationViewData() { - } + private NotificationViewData() {} public abstract long getViewDataId(); @@ -48,19 +44,19 @@ public abstract class NotificationViewData { private final Notification.Type type; private final String id; private final Account account; - @Nullable - private final StatusViewData.Concrete statusViewData; - @Nullable - private final String emoji; - @Nullable - private final String emojiUrl; - @Nullable - private final Account target; // move notification - - public Concrete(Notification.Type type, String id, Account account, - @Nullable StatusViewData.Concrete statusViewData, - @Nullable String emoji, @Nullable String emojiUrl, - @Nullable Account target) { + @Nullable private final StatusViewData.Concrete statusViewData; + @Nullable private final String emoji; + @Nullable private final String emojiUrl; + @Nullable private final Account target; // move notification + + public Concrete( + Notification.Type type, + String id, + Account account, + @Nullable StatusViewData.Concrete statusViewData, + @Nullable String emoji, + @Nullable String emojiUrl, + @Nullable Account target) { this.type = type; this.id = id; this.account = account; @@ -89,7 +85,7 @@ public abstract class NotificationViewData { @Nullable public String getEmoji() { - return emoji; + return emoji; } @Nullable @@ -112,15 +108,19 @@ public abstract class NotificationViewData { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Concrete concrete = (Concrete) o; - return type == concrete.type && - Objects.equals(id, concrete.id) && - account.getId().equals(concrete.account.getId()) && - (emoji != null && concrete.emoji != null && emoji.equals(concrete.emoji)) && - (emojiUrl != null && concrete.emojiUrl != null && emojiUrl.equals(concrete.emojiUrl)) && - (target != null && concrete.target != null && target.getId().equals(concrete.target.getId())) && - (statusViewData == concrete.statusViewData || - statusViewData != null && - statusViewData.deepEquals(concrete.statusViewData)); + return type == concrete.type + && Objects.equals(id, concrete.id) + && account.getId().equals(concrete.account.getId()) + && (emoji != null && concrete.emoji != null && emoji.equals(concrete.emoji)) + && (emojiUrl != null + && concrete.emojiUrl != null + && emojiUrl.equals(concrete.emojiUrl)) + && (target != null + && concrete.target != null + && target.getId().equals(concrete.target.getId())) + && (statusViewData == concrete.statusViewData + || statusViewData != null + && statusViewData.deepEquals(concrete.statusViewData)); } @Override diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java b/husky/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java index 0238a82..5cc666d 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java +++ b/husky/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java @@ -38,13 +38,12 @@ import java.util.List; import java.util.Objects; /** - * Class to represent data required to display either a notification or a placeholder. - * It is either a {@link StatusViewData.Concrete} or a {@link StatusViewData.Placeholder}. + * Class to represent data required to display either a notification or a placeholder. It is either + * a {@link StatusViewData.Concrete} or a {@link StatusViewData.Placeholder}. */ public abstract class StatusViewData { - private StatusViewData() { - } + private StatusViewData() {} public abstract long getViewDataId(); @@ -59,14 +58,11 @@ public abstract class StatusViewData { final boolean reblogged; final boolean favourited; final boolean bookmarked; - @Nullable - private final String spoilerText; + @Nullable private final String spoilerText; private final Status.Visibility visibility; private final List<Attachment> attachments; - @Nullable - private final String rebloggedByUsername; - @Nullable - private final String rebloggedAvatar; + @Nullable private final String rebloggedByUsername; + @Nullable private final String rebloggedAvatar; private final boolean isSensitive; final boolean isExpanded; private final boolean isShowingContent; @@ -77,55 +73,81 @@ public abstract class StatusViewData { private final Date editedAt; private final int reblogsCount; private final int favouritesCount; - @Nullable - private final String inReplyToId; - @Nullable - private final String inReplyToAccountAcct; + @Nullable private final String inReplyToId; + @Nullable private final String inReplyToAccountAcct; // I would rather have something else but it would be too much of a rewrite - @Nullable - private final Status.Mention[] mentions; + @Nullable private final Status.Mention[] mentions; private final String senderId; private final boolean rebloggingEnabled; private final Status.Application application; private final List<Emoji> statusEmojis; private final List<Emoji> accountEmojis; private final List<Emoji> rebloggedByAccountEmojis; - @Nullable - private final Card card; + @Nullable private final Card card; private final boolean isCollapsible; - /** - * Whether the status meets the requirement to be collapse - */ + /** Whether the status meets the requirement to be collapse */ final boolean isCollapsed; - /** - * Whether the status is shown partially or fully - */ - @Nullable - private final PollViewData poll; + /** Whether the status is shown partially or fully */ + @Nullable private final PollViewData poll; + private final boolean isBot; private final boolean isMuted; /* user toggle */ private final boolean isThreadMuted; /* thread_muted state got from backend */ private final boolean isUserMuted; /* muted state got from backend */ private final String conversationId; - @Nullable - private final List<EmojiReaction> emojiReactions; + @Nullable private final List<EmojiReaction> emojiReactions; private final boolean parentVisible; - public Concrete(String id, Spanned content, boolean reblogged, boolean favourited, boolean bookmarked, - @Nullable String spoilerText, Status.Visibility visibility, List<Attachment> attachments, - @Nullable String rebloggedByUsername, @Nullable String rebloggedAvatar, boolean sensitive, boolean isExpanded, - boolean isShowingContent, String userFullName, String nickname, String avatar, - Date createdAt, Date editedAt, int reblogsCount, int favouritesCount, @Nullable String inReplyToId, - @Nullable String inReplyToAccountAcct, @Nullable Status.Mention[] mentions, String senderId, boolean rebloggingEnabled, - Status.Application application, List<Emoji> statusEmojis, List<Emoji> accountEmojis, List<Emoji> rebloggedByAccountEmojis, @Nullable Card card, - boolean isCollapsible, boolean isCollapsed, @Nullable PollViewData poll, boolean isBot, boolean isMuted, boolean isThreadMuted, - boolean isUserMuted, String conversationId, @Nullable List<EmojiReaction> emojiReactions, boolean parentVisible) { + public Concrete( + String id, + Spanned content, + boolean reblogged, + boolean favourited, + boolean bookmarked, + @Nullable String spoilerText, + Status.Visibility visibility, + List<Attachment> attachments, + @Nullable String rebloggedByUsername, + @Nullable String rebloggedAvatar, + boolean sensitive, + boolean isExpanded, + boolean isShowingContent, + String userFullName, + String nickname, + String avatar, + Date createdAt, + Date editedAt, + int reblogsCount, + int favouritesCount, + @Nullable String inReplyToId, + @Nullable String inReplyToAccountAcct, + @Nullable Status.Mention[] mentions, + String senderId, + boolean rebloggingEnabled, + Status.Application application, + List<Emoji> statusEmojis, + List<Emoji> accountEmojis, + List<Emoji> rebloggedByAccountEmojis, + @Nullable Card card, + boolean isCollapsible, + boolean isCollapsed, + @Nullable PollViewData poll, + boolean isBot, + boolean isMuted, + boolean isThreadMuted, + boolean isUserMuted, + String conversationId, + @Nullable List<EmojiReaction> emojiReactions, + boolean parentVisible) { this.id = id; - if(Build.VERSION.SDK_INT == Build.VERSION_CODES.M) { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.M) { // https://github.com/tuskyapp/Tusky/issues/563 this.content = replaceCrashingCharacters(content); - this.spoilerText = spoilerText == null ? null : replaceCrashingCharacters(spoilerText).toString(); + this.spoilerText = + spoilerText == null + ? null + : replaceCrashingCharacters(spoilerText).toString(); this.nickname = replaceCrashingCharacters(nickname).toString(); } else { this.content = content; @@ -263,7 +285,7 @@ public abstract class StatusViewData { } public String getInReplyToAccountAcct() { - if(inReplyToAccountAcct != null) { + if (inReplyToAccountAcct != null) { return inReplyToAccountAcct; } return ""; @@ -308,8 +330,8 @@ public abstract class StatusViewData { } /** - * Specifies whether the content of this post is allowed to be collapsed or if it should show - * all content regardless. + * Specifies whether the content of this post is allowed to be collapsed or if it should + * show all content regardless. * * @return Whether the post is collapsible or never collapsed. */ @@ -318,8 +340,8 @@ public abstract class StatusViewData { } /** - * Specifies whether the content of this post is currently limited in visibility to the first - * 500 characters or not. + * Specifies whether the content of this post is currently limited in visibility to the + * first 500 characters or not. * * @return Whether the post is collapsed or fully expanded. */ @@ -356,48 +378,48 @@ public abstract class StatusViewData { } public boolean deepEquals(StatusViewData o) { - if(this == o) return true; - if(o == null || getClass() != o.getClass()) return false; + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; Concrete concrete = (Concrete) o; - return reblogged == concrete.reblogged && - favourited == concrete.favourited && - bookmarked == concrete.bookmarked && - isSensitive == concrete.isSensitive && - isExpanded == concrete.isExpanded && - isShowingContent == concrete.isShowingContent && - isBot == concrete.isBot && - reblogsCount == concrete.reblogsCount && - favouritesCount == concrete.favouritesCount && - rebloggingEnabled == concrete.rebloggingEnabled && - Objects.equals(id, concrete.id) && - Objects.equals(content, concrete.content) && - Objects.equals(spoilerText, concrete.spoilerText) && - visibility == concrete.visibility && - Objects.equals(attachments, concrete.attachments) && - Objects.equals(rebloggedByUsername, concrete.rebloggedByUsername) && - Objects.equals(rebloggedAvatar, concrete.rebloggedAvatar) && - Objects.equals(userFullName, concrete.userFullName) && - Objects.equals(nickname, concrete.nickname) && - Objects.equals(avatar, concrete.avatar) && - Objects.equals(createdAt, concrete.createdAt) && - Objects.equals(editedAt, concrete.editedAt) && - Objects.equals(inReplyToId, concrete.inReplyToId) && - Objects.equals(inReplyToAccountAcct, concrete.inReplyToAccountAcct) && - Arrays.equals(mentions, concrete.mentions) && - Objects.equals(senderId, concrete.senderId) && - Objects.equals(application, concrete.application) && - Objects.equals(statusEmojis, concrete.statusEmojis) && - Objects.equals(accountEmojis, concrete.accountEmojis) && - Objects.equals(rebloggedByAccountEmojis, concrete.rebloggedByAccountEmojis) && - Objects.equals(card, concrete.card) && - Objects.equals(poll, concrete.poll) && - isCollapsed == concrete.isCollapsed && - isMuted == concrete.isMuted && - isThreadMuted == concrete.isThreadMuted && - isUserMuted == concrete.isUserMuted && - conversationId == concrete.conversationId && - Objects.equals(emojiReactions, concrete.emojiReactions) && - parentVisible == concrete.parentVisible; + return reblogged == concrete.reblogged + && favourited == concrete.favourited + && bookmarked == concrete.bookmarked + && isSensitive == concrete.isSensitive + && isExpanded == concrete.isExpanded + && isShowingContent == concrete.isShowingContent + && isBot == concrete.isBot + && reblogsCount == concrete.reblogsCount + && favouritesCount == concrete.favouritesCount + && rebloggingEnabled == concrete.rebloggingEnabled + && Objects.equals(id, concrete.id) + && Objects.equals(content, concrete.content) + && Objects.equals(spoilerText, concrete.spoilerText) + && visibility == concrete.visibility + && Objects.equals(attachments, concrete.attachments) + && Objects.equals(rebloggedByUsername, concrete.rebloggedByUsername) + && Objects.equals(rebloggedAvatar, concrete.rebloggedAvatar) + && Objects.equals(userFullName, concrete.userFullName) + && Objects.equals(nickname, concrete.nickname) + && Objects.equals(avatar, concrete.avatar) + && Objects.equals(createdAt, concrete.createdAt) + && Objects.equals(editedAt, concrete.editedAt) + && Objects.equals(inReplyToId, concrete.inReplyToId) + && Objects.equals(inReplyToAccountAcct, concrete.inReplyToAccountAcct) + && Arrays.equals(mentions, concrete.mentions) + && Objects.equals(senderId, concrete.senderId) + && Objects.equals(application, concrete.application) + && Objects.equals(statusEmojis, concrete.statusEmojis) + && Objects.equals(accountEmojis, concrete.accountEmojis) + && Objects.equals(rebloggedByAccountEmojis, concrete.rebloggedByAccountEmojis) + && Objects.equals(card, concrete.card) + && Objects.equals(poll, concrete.poll) + && isCollapsed == concrete.isCollapsed + && isMuted == concrete.isMuted + && isThreadMuted == concrete.isThreadMuted + && isUserMuted == concrete.isUserMuted + && conversationId == concrete.conversationId + && Objects.equals(emojiReactions, concrete.emojiReactions) + && parentVisible == concrete.parentVisible; } static Spanned replaceCrashingCharacters(Spanned content) { @@ -409,17 +431,17 @@ public abstract class StatusViewData { SpannableStringBuilder builder = null; int length = content.length(); - for(int index = 0; index < length; ++index) { + for (int index = 0; index < length; ++index) { char character = content.charAt(index); // If there are more than one or two, switch to a map - if(character == SOFT_HYPHEN) { - if(!replacing) { + if (character == SOFT_HYPHEN) { + if (!replacing) { replacing = true; builder = new SpannableStringBuilder(content, 0, index); } builder.append(ASCII_HYPHEN); - } else if(replacing) { + } else if (replacing) { builder.append(character); } } @@ -452,15 +474,15 @@ public abstract class StatusViewData { @Override public boolean deepEquals(StatusViewData other) { - if(!(other instanceof Placeholder)) return false; + if (!(other instanceof Placeholder)) return false; Placeholder that = (Placeholder) other; return isLoading == that.isLoading && id.equals(that.id); } @Override public boolean equals(Object o) { - if(this == o) return true; - if(o == null || getClass() != o.getClass()) return false; + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; Placeholder that = (Placeholder) o; @@ -507,14 +529,11 @@ public abstract class StatusViewData { private List<Emoji> rebloggedByAccountEmojis; private Card card; private boolean isCollapsible; - /** - * Whether the status meets the requirement to be collapsed - */ + /** Whether the status meets the requirement to be collapsed */ private boolean isCollapsed; - /** - * Whether the status is shown partially or fully - */ + /** Whether the status is shown partially or fully */ private PollViewData poll; + private boolean isBot; private boolean isMuted; private boolean isThreadMuted; @@ -523,8 +542,7 @@ public abstract class StatusViewData { private List<EmojiReaction> emojiReactions; private boolean parentVisible; - public Builder() { - } + public Builder() {} public Builder(final StatusViewData.Concrete viewData) { id = viewData.id; @@ -534,7 +552,8 @@ public abstract class StatusViewData { bookmarked = viewData.bookmarked; spoilerText = viewData.spoilerText; visibility = viewData.visibility; - attachments = viewData.attachments == null ? null : new ArrayList<>(viewData.attachments); + attachments = + viewData.attachments == null ? null : new ArrayList<>(viewData.attachments); rebloggedByUsername = viewData.rebloggedByUsername; rebloggedAvatar = viewData.rebloggedAvatar; isSensitive = viewData.isSensitive; @@ -728,8 +747,8 @@ public abstract class StatusViewData { } /** - * Configure the {@link com.keylesspalace.tusky.viewdata.StatusViewData} to support collapsing - * its content limiting the visible length when collapsed at 500 characters, + * Configure the {@link com.keylesspalace.tusky.viewdata.StatusViewData} to support + * collapsing its content limiting the visible length when collapsed at 500 characters, * * @param collapsible Whether the status should support being collapsed or not. * @return This {@link com.keylesspalace.tusky.viewdata.StatusViewData.Builder} instance. @@ -740,8 +759,9 @@ public abstract class StatusViewData { } /** - * Configure the {@link com.keylesspalace.tusky.viewdata.StatusViewData} to start in a collapsed - * state, hiding partially the content of the post if it exceeds a certain amount of characters. + * Configure the {@link com.keylesspalace.tusky.viewdata.StatusViewData} to start in a + * collapsed state, hiding partially the content of the post if it exceeds a certain amount + * of characters. * * @param collapsed Whether to show the full content of the status or not. * @return This {@link com.keylesspalace.tusky.viewdata.StatusViewData.Builder} instance. @@ -782,16 +802,51 @@ public abstract class StatusViewData { } public StatusViewData.Concrete createStatusViewData() { - if(this.statusEmojis == null) statusEmojis = Collections.emptyList(); - if(this.accountEmojis == null) accountEmojis = Collections.emptyList(); - if(this.createdAt == null) createdAt = new Date(); - - return new StatusViewData.Concrete(id, content, reblogged, favourited, bookmarked, spoilerText, - visibility, attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded, - isShowingContent, userFullName, nickname, avatar, createdAt, editedAt, reblogsCount, - favouritesCount, inReplyToId, inReplyToAccountAcct, mentions, senderId, rebloggingEnabled, application, - statusEmojis, accountEmojis, rebloggedByAccountEmojis, card, isCollapsible, isCollapsed, poll, isBot, isMuted, isThreadMuted, - isUserMuted, conversationId, emojiReactions, parentVisible); + if (this.statusEmojis == null) statusEmojis = Collections.emptyList(); + if (this.accountEmojis == null) accountEmojis = Collections.emptyList(); + if (this.createdAt == null) createdAt = new Date(); + + return new StatusViewData.Concrete( + id, + content, + reblogged, + favourited, + bookmarked, + spoilerText, + visibility, + attachments, + rebloggedByUsername, + rebloggedAvatar, + isSensitive, + isExpanded, + isShowingContent, + userFullName, + nickname, + avatar, + createdAt, + editedAt, + reblogsCount, + favouritesCount, + inReplyToId, + inReplyToAccountAcct, + mentions, + senderId, + rebloggingEnabled, + application, + statusEmojis, + accountEmojis, + rebloggedByAccountEmojis, + card, + isCollapsible, + isCollapsed, + poll, + isBot, + isMuted, + isThreadMuted, + isUserMuted, + conversationId, + emojiReactions, + parentVisible); } } } diff --git a/husky/app/src/test/java/com/keylesspalace/tusky/util/ViewPager2FixTest.java b/husky/app/src/test/java/com/keylesspalace/tusky/util/ViewPager2FixTest.java index 859f25c..50edb50 100644 --- a/husky/app/src/test/java/com/keylesspalace/tusky/util/ViewPager2FixTest.java +++ b/husky/app/src/test/java/com/keylesspalace/tusky/util/ViewPager2FixTest.java @@ -10,7 +10,7 @@ public class ViewPager2FixTest { public void getViewPagerRecyclerViewFieldTest() { try { Field f = ViewPager2Fix.getViewPagerRecyclerViewField(); - } catch(Exception e) { + } catch (Exception e) { Assert.fail("asdf"); } } @@ -19,7 +19,7 @@ public class ViewPager2FixTest { public void getRecyclerViewTouchSlopFieldTest() { try { Field f = ViewPager2Fix.getRecyclerViewTouchSlopField(); - } catch(Exception e) { + } catch (Exception e) { Assert.fail("asdf"); } } |