Convert some adapters to Kotlin (#2187)

* Rename .java adapters to .kt

* Convert Account adapters to Kotlin

* Apply feedback for adapter refactoring
This commit is contained in:
Ivan Kupalov 2021-06-20 10:18:40 +02:00 committed by GitHub
parent 6d4f5ad027
commit 837ee2e40d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 589 additions and 689 deletions

View File

@ -1,116 +0,0 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.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;
public abstract class AccountAdapter extends RecyclerView.Adapter {
static final int VIEW_TYPE_ACCOUNT = 0;
static final int VIEW_TYPE_FOOTER = 1;
List<Account> accountList;
AccountActionListener accountActionListener;
private boolean bottomLoading;
protected final boolean animateEmojis;
protected final boolean animateAvatar;
AccountAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) {
this.accountList = new ArrayList<>();
this.accountActionListener = accountActionListener;
this.animateAvatar = animateAvatar;
this.animateEmojis = animateEmojis;
bottomLoading = false;
}
@Override
public int getItemCount() {
return accountList.size() + (bottomLoading ? 1 : 0);
}
@Override
public int getItemViewType(int position) {
if (position == accountList.size() && bottomLoading) {
return VIEW_TYPE_FOOTER;
} else {
return VIEW_TYPE_ACCOUNT;
}
}
public void update(@NonNull List<Account> newAccounts) {
accountList = ListUtils.removeDuplicates(newAccounts);
notifyDataSetChanged();
}
public void addItems(@NonNull List<Account> newAccounts) {
int end = accountList.size();
Account last = accountList.get(end - 1);
if (last != null && !findAccount(newAccounts, last.getId())) {
accountList.addAll(newAccounts);
notifyItemRangeInserted(end, newAccounts.size());
}
}
public void setBottomLoading(boolean loading) {
boolean wasLoading = bottomLoading;
if(wasLoading == loading) {
return;
}
bottomLoading = loading;
if(loading) {
notifyItemInserted(accountList.size());
} else {
notifyItemRemoved(accountList.size());
}
}
private static boolean findAccount(@NonNull List<Account> accounts, String id) {
for (Account account : accounts) {
if (account.getId().equals(id)) {
return true;
}
}
return false;
}
@Nullable
public Account removeItem(int position) {
if (position < 0 || position >= accountList.size()) {
return null;
}
Account account = accountList.remove(position);
notifyItemRemoved(position);
return account;
}
public void addItem(@NonNull Account account, int position) {
if (position < 0 || position > accountList.size()) {
return;
}
accountList.add(position, account);
notifyItemInserted(position);
}
}

View File

@ -0,0 +1,125 @@
/* Copyright 2021 Tusky Contributors.
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
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.removeDuplicates
/** Generic adapter with bottom loading indicator. */
abstract class AccountAdapter<AVH : RecyclerView.ViewHolder> internal constructor(
var accountActionListener: AccountActionListener,
protected val animateAvatar: Boolean,
protected val animateEmojis: Boolean
) : RecyclerView.Adapter<RecyclerView.ViewHolder?>() {
var accountList = mutableListOf<Account>()
private var bottomLoading: Boolean = false
override fun getItemCount(): Int {
return accountList.size + if (bottomLoading) 1 else 0
}
abstract fun createAccountViewHolder(parent: ViewGroup): AVH
abstract fun onBindAccountViewHolder(viewHolder: AVH, position: Int)
final override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
@Suppress("UNCHECKED_CAST")
this.onBindAccountViewHolder(holder as AVH, position)
}
}
final override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): RecyclerView.ViewHolder {
return when (viewType) {
VIEW_TYPE_ACCOUNT -> this.createAccountViewHolder(parent)
VIEW_TYPE_FOOTER -> this.createFooterViewHolder(parent)
else -> error("Unknown item type: $viewType")
}
}
private fun createFooterViewHolder(
parent: ViewGroup,
): RecyclerView.ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_footer, parent, false)
return LoadingFooterViewHolder(view)
}
override fun getItemViewType(position: Int): Int {
return if (position == accountList.size && bottomLoading) {
VIEW_TYPE_FOOTER
} else {
VIEW_TYPE_ACCOUNT
}
}
fun update(newAccounts: List<Account>) {
accountList = removeDuplicates(newAccounts)
notifyDataSetChanged()
}
fun addItems(newAccounts: List<Account>) {
val end = accountList.size
val last = accountList[end - 1]
if (newAccounts.none { it.id == last.id }) {
accountList.addAll(newAccounts)
notifyItemRangeInserted(end, newAccounts.size)
}
}
fun setBottomLoading(loading: Boolean) {
val wasLoading = bottomLoading
if (wasLoading == loading) {
return
}
bottomLoading = loading
if (loading) {
notifyItemInserted(accountList.size)
} else {
notifyItemRemoved(accountList.size)
}
}
fun removeItem(position: Int): Account? {
if (position < 0 || position >= accountList.size) {
return null
}
val account = accountList.removeAt(position)
notifyItemRemoved(position)
return account
}
fun addItem(account: Account, position: Int) {
if (position < 0 || position > accountList.size) {
return
}
accountList.add(position, account)
notifyItemInserted(position)
}
companion object {
const val VIEW_TYPE_ACCOUNT = 0
const val VIEW_TYPE_FOOTER = 1
}
}

View File

@ -1,106 +0,0 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.adapter;
import android.view.LayoutInflater;
import android.view.View;
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;
import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.keylesspalace.tusky.util.ImageLoadingHelper;
public class BlocksAdapter extends AccountAdapter {
public BlocksAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) {
super(accountActionListener, animateAvatar, animateEmojis);
}
@NonNull
@Override
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);
}
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
BlockedUserViewHolder holder = (BlockedUserViewHolder) viewHolder;
holder.setupWithAccount(accountList.get(position), animateAvatar, animateEmojis);
holder.setupActionListener(accountActionListener);
}
}
static class BlockedUserViewHolder extends RecyclerView.ViewHolder {
private ImageView avatar;
private TextView username;
private TextView displayName;
private ImageButton unblock;
private String id;
BlockedUserViewHolder(View itemView) {
super(itemView);
avatar = itemView.findViewById(R.id.blocked_user_avatar);
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);
}
void setupWithAccount(Account account, boolean animateAvatar, boolean animateEmojis) {
id = account.getId();
CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName, animateEmojis);
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);
ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius, animateAvatar);
}
void setupActionListener(final AccountActionListener listener) {
unblock.setOnClickListener(v -> {
int position = getBindingAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
listener.onBlock(false, id, position);
}
});
itemView.setOnClickListener(v -> listener.onViewAccount(id));
}
}
}

View File

@ -0,0 +1,80 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
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.emojify
import com.keylesspalace.tusky.util.loadAvatar
/** Displays a list of blocked accounts. */
class BlocksAdapter(
accountActionListener: AccountActionListener,
animateAvatar: Boolean,
animateEmojis: Boolean
) : AccountAdapter<BlocksAdapter.BlockedUserViewHolder>(
accountActionListener,
animateAvatar,
animateEmojis
) {
override fun createAccountViewHolder(parent: ViewGroup): BlockedUserViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_blocked_user, parent, false)
return BlockedUserViewHolder(view)
}
override fun onBindAccountViewHolder(viewHolder: BlockedUserViewHolder, position: Int) {
viewHolder.setupWithAccount(accountList[position], animateAvatar, animateEmojis)
viewHolder.setupActionListener(accountActionListener)
}
class BlockedUserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val avatar: ImageView = itemView.findViewById(R.id.blocked_user_avatar)
private val username: TextView = itemView.findViewById(R.id.blocked_user_username)
private val displayName: TextView = itemView.findViewById(R.id.blocked_user_display_name)
private val unblock: ImageButton = itemView.findViewById(R.id.blocked_user_unblock)
private var id: String? = null
fun setupWithAccount(account: Account, animateAvatar: Boolean, animateEmojis: Boolean) {
id = account.id
val emojifiedName = account.name.emojify(account.emojis, displayName, animateEmojis)
displayName.text = emojifiedName
val format = username.context.getString(R.string.status_username_format)
val formattedUsername = String.format(format, account.username)
username.text = formattedUsername
val avatarRadius = avatar.context.resources
.getDimensionPixelSize(R.dimen.avatar_radius_48dp)
loadAvatar(account.avatar, avatar, avatarRadius, animateAvatar)
}
fun setupActionListener(listener: AccountActionListener) {
unblock.setOnClickListener {
val position = bindingAdapterPosition
if (position != RecyclerView.NO_POSITION) {
listener.onBlock(false, id, position)
}
}
itemView.setOnClickListener { listener.onViewAccount(id) }
}
}
}

View File

@ -1,61 +0,0 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.adapter;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.interfaces.AccountActionListener;
/** Both for follows and following lists. */
public class FollowAdapter extends AccountAdapter {
public FollowAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) {
super(accountActionListener, animateAvatar, animateEmojis);
}
@NonNull
@Override
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);
}
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
AccountViewHolder holder = (AccountViewHolder) viewHolder;
holder.setupWithAccount(accountList.get(position), animateAvatar, animateEmojis);
holder.setupActionListener(accountActionListener);
}
}
}

View File

@ -0,0 +1,39 @@
/* Copyright 2021 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.interfaces.AccountActionListener
/** Displays either a follows or following list. */
class FollowAdapter(
accountActionListener: AccountActionListener,
animateAvatar: Boolean,
animateEmojis: Boolean
) : AccountAdapter<AccountViewHolder>(accountActionListener, animateAvatar, animateEmojis) {
override fun createAccountViewHolder(parent: ViewGroup): AccountViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_account, parent, false)
return AccountViewHolder(view)
}
override fun onBindAccountViewHolder(viewHolder: AccountViewHolder, position: Int) {
viewHolder.setupWithAccount(accountList[position], animateAvatar, animateEmojis)
viewHolder.setupActionListener(accountActionListener)
}
}

View File

@ -1,60 +0,0 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.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;
public class FollowRequestsAdapter extends AccountAdapter {
public FollowRequestsAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) {
super(accountActionListener, animateAvatar, animateEmojis);
}
@NonNull
@Override
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);
}
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
holder.setupWithAccount(accountList.get(position), animateAvatar, animateEmojis);
holder.setupActionListener(accountActionListener, accountList.get(position).getId());
}
}
}

View File

@ -0,0 +1,41 @@
/* Copyright 2021 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
import com.keylesspalace.tusky.interfaces.AccountActionListener
/** Displays a list of follow requests with accept/reject buttons. */
class FollowRequestsAdapter(
accountActionListener: AccountActionListener,
animateAvatar: Boolean,
animateEmojis: Boolean
) : AccountAdapter<FollowRequestViewHolder>(accountActionListener, animateAvatar, animateEmojis) {
override fun createAccountViewHolder(parent: ViewGroup): FollowRequestViewHolder {
val binding = ItemFollowRequestBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return FollowRequestViewHolder(binding, false)
}
override fun onBindAccountViewHolder(viewHolder: FollowRequestViewHolder, position: Int) {
viewHolder.setupWithAccount(accountList[position], animateAvatar, animateEmojis)
viewHolder.setupActionListener(accountActionListener, accountList[position].id)
}
}

View File

@ -1,131 +0,0 @@
package com.keylesspalace.tusky.adapter;
import android.view.LayoutInflater;
import android.view.View;
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.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 {
private HashMap<String, Boolean> mutingNotificationsMap;
public MutesAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) {
super(accountActionListener, animateAvatar, animateEmojis);
mutingNotificationsMap = new HashMap<String, Boolean>();
}
@NonNull
@Override
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);
}
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
MutedUserViewHolder holder = (MutedUserViewHolder) viewHolder;
Account account = accountList.get(position);
holder.setupWithAccount(account, mutingNotificationsMap.get(account.getId()), animateAvatar, animateEmojis);
holder.setupActionListener(accountActionListener);
}
}
public void updateMutingNotifications(String id, boolean mutingNotifications, int position) {
mutingNotificationsMap.put(id, mutingNotifications);
notifyItemChanged(position);
}
public void updateMutingNotificationsMap(HashMap<String, Boolean> newMutingNotificationsMap) {
mutingNotificationsMap.putAll(newMutingNotificationsMap);
notifyDataSetChanged();
}
static class MutedUserViewHolder extends RecyclerView.ViewHolder {
private ImageView avatar;
private TextView username;
private TextView displayName;
private ImageButton unmute;
private ImageButton muteNotifications;
private String id;
private boolean notifications;
MutedUserViewHolder(View itemView) {
super(itemView);
avatar = itemView.findViewById(R.id.muted_user_avatar);
username = itemView.findViewById(R.id.muted_user_username);
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);
}
void setupWithAccount(Account account, Boolean mutingNotifications, boolean animateAvatar, boolean animateEmojis) {
id = account.getId();
CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName, animateEmojis);
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);
ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius, animateAvatar);
String unmuteString = unmute.getContext().getString(R.string.action_unmute_desc, formattedUsername);
unmute.setContentDescription(unmuteString);
ViewCompat.setTooltipText(unmute, unmuteString);
if (mutingNotifications == null) {
muteNotifications.setEnabled(false);
notifications = true;
} else {
muteNotifications.setEnabled(true);
notifications = mutingNotifications;
}
if (notifications) {
muteNotifications.setImageResource(R.drawable.ic_notifications_24dp);
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);
muteNotifications.setContentDescription(muteNotificationsString);
ViewCompat.setTooltipText(muteNotifications, muteNotificationsString);
}
}
void setupActionListener(final AccountActionListener listener) {
unmute.setOnClickListener(v -> listener.onMute(false, id, getBindingAdapterPosition(), false));
muteNotifications.setOnClickListener(
v -> listener.onMute(true, id, getBindingAdapterPosition(), !notifications));
itemView.setOnClickListener(v -> listener.onViewAccount(id));
}
}
}

View File

@ -0,0 +1,132 @@
package com.keylesspalace.tusky.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.ViewCompat
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.emojify
import com.keylesspalace.tusky.util.loadAvatar
import java.util.*
/**
* Displays a list of muted accounts with mute/unmute account and mute/unmute notifications
* buttons.
* */
class MutesAdapter(
accountActionListener: AccountActionListener,
animateAvatar: Boolean,
animateEmojis: Boolean
) : AccountAdapter<MutesAdapter.MutedUserViewHolder>(
accountActionListener,
animateAvatar,
animateEmojis
) {
private val mutingNotificationsMap = HashMap<String, Boolean>()
override fun createAccountViewHolder(parent: ViewGroup): MutedUserViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_muted_user, parent, false)
return MutedUserViewHolder(view)
}
override fun onBindAccountViewHolder(viewHolder: MutedUserViewHolder, position: Int) {
val account = accountList[position]
viewHolder.setupWithAccount(
account,
mutingNotificationsMap[account.id],
animateAvatar,
animateEmojis
)
viewHolder.setupActionListener(accountActionListener)
}
fun updateMutingNotifications(id: String, mutingNotifications: Boolean, position: Int) {
mutingNotificationsMap[id] = mutingNotifications
notifyItemChanged(position)
}
fun updateMutingNotificationsMap(newMutingNotificationsMap: HashMap<String, Boolean>?) {
mutingNotificationsMap.putAll(newMutingNotificationsMap!!)
notifyDataSetChanged()
}
class MutedUserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val avatar: ImageView = itemView.findViewById(R.id.muted_user_avatar)
private val username: TextView = itemView.findViewById(R.id.muted_user_username)
private val displayName: TextView = itemView.findViewById(R.id.muted_user_display_name)
private val unmute: ImageButton = itemView.findViewById(R.id.muted_user_unmute)
private val muteNotifications: ImageButton =
itemView.findViewById(R.id.muted_user_mute_notifications)
private var id: String? = null
private var notifications = false
fun setupWithAccount(
account: Account,
mutingNotifications: Boolean?,
animateAvatar: Boolean,
animateEmojis: Boolean
) {
id = account.id
val emojifiedName = account.name.emojify(account.emojis, displayName, animateEmojis)
displayName.text = emojifiedName
val format = username.context.getString(R.string.status_username_format)
val formattedUsername = String.format(format, account.username)
username.text = formattedUsername
val avatarRadius = avatar.context.resources
.getDimensionPixelSize(R.dimen.avatar_radius_48dp)
loadAvatar(account.avatar, avatar, avatarRadius, animateAvatar)
val unmuteString =
unmute.context.getString(R.string.action_unmute_desc, formattedUsername)
unmute.contentDescription = unmuteString
ViewCompat.setTooltipText(unmute, unmuteString)
if (mutingNotifications == null) {
muteNotifications.isEnabled = false
notifications = true
} else {
muteNotifications.isEnabled = true
notifications = mutingNotifications
}
if (notifications) {
muteNotifications.setImageResource(R.drawable.ic_notifications_24dp)
val unmuteNotificationsString = muteNotifications.context
.getString(R.string.action_unmute_notifications_desc, formattedUsername)
muteNotifications.contentDescription = unmuteNotificationsString
ViewCompat.setTooltipText(muteNotifications, unmuteNotificationsString)
} else {
muteNotifications.setImageResource(R.drawable.ic_notifications_off_24dp)
val muteNotificationsString = muteNotifications.context
.getString(R.string.action_mute_notifications_desc, formattedUsername)
muteNotifications.contentDescription = muteNotificationsString
ViewCompat.setTooltipText(muteNotifications, muteNotificationsString)
}
}
fun setupActionListener(listener: AccountActionListener) {
unmute.setOnClickListener {
listener.onMute(
false,
id,
bindingAdapterPosition,
false
)
}
muteNotifications.setOnClickListener {
listener.onMute(
true,
id,
bindingAdapterPosition,
!notifications
)
}
itemView.setOnClickListener { listener.onViewAccount(id) }
}
}
}

View File

@ -1,4 +1,4 @@
/* Copyright 2017 Andrew Dawson
/* Copyright 2021 Tusky Contributors
*
* This file is a part of Tusky.
*

View File

@ -1,49 +0,0 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.adapter;
import androidx.recyclerview.widget.RecyclerView;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
public final class PlaceholderViewHolder extends RecyclerView.ViewHolder {
private Button loadMoreButton;
private ProgressBar progressBar;
public PlaceholderViewHolder(View itemView) {
super(itemView);
loadMoreButton = itemView.findViewById(R.id.button_load_more);
progressBar = itemView.findViewById(R.id.progressBar);
}
public void setup(final StatusActionListener listener, boolean progress) {
loadMoreButton.setVisibility(progress ? View.GONE : View.VISIBLE);
progressBar.setVisibility(progress ? View.VISIBLE : View.GONE);
loadMoreButton.setEnabled(true);
loadMoreButton.setOnClickListener(v -> {
loadMoreButton.setEnabled(false);
listener.onLoadMore(getBindingAdapterPosition());
});
}
}

View File

@ -0,0 +1,41 @@
/* Copyright 2021 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.adapter
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.StatusActionListener
/**
* Placeholder for different timelines.
* Either displays "load more" button or a progress indicator.
**/
class PlaceholderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val loadMoreButton: Button = itemView.findViewById(R.id.button_load_more)
private val progressBar: ProgressBar = itemView.findViewById(R.id.progressBar)
fun setup(listener: StatusActionListener, progress: Boolean) {
loadMoreButton.visibility = if (progress) View.GONE else View.VISIBLE
progressBar.visibility = if (progress) View.VISIBLE else View.GONE
loadMoreButton.isEnabled = true
loadMoreButton.setOnClickListener { v: View? ->
loadMoreButton.isEnabled = false
listener.onLoadMore(bindingAdapterPosition)
}
}
}

View File

@ -1,164 +0,0 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.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;
public class ThreadAdapter extends RecyclerView.Adapter {
private static final int VIEW_TYPE_STATUS = 0;
private static final int VIEW_TYPE_STATUS_DETAILED = 1;
private List<StatusViewData.Concrete> statuses;
private StatusDisplayOptions statusDisplayOptions;
private StatusActionListener statusActionListener;
private int detailedStatusPosition;
public ThreadAdapter(StatusDisplayOptions statusDisplayOptions, StatusActionListener listener) {
this.statusDisplayOptions = statusDisplayOptions;
this.statusActionListener = listener;
this.statuses = new ArrayList<>();
detailedStatusPosition = RecyclerView.NO_POSITION;
}
@NonNull
@Override
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);
}
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
StatusViewData.Concrete status = statuses.get(position);
if (position == detailedStatusPosition) {
StatusDetailedViewHolder holder = (StatusDetailedViewHolder) viewHolder;
holder.setupWithStatus(status, statusActionListener, statusDisplayOptions);
} else {
StatusViewHolder holder = (StatusViewHolder) viewHolder;
holder.setupWithStatus(status, statusActionListener, statusDisplayOptions);
}
}
@Override
public int getItemViewType(int position) {
if (position == detailedStatusPosition) {
return VIEW_TYPE_STATUS_DETAILED;
} else {
return VIEW_TYPE_STATUS;
}
}
@Override
public int getItemCount() {
return statuses.size();
}
public void setStatuses(List<StatusViewData.Concrete> statuses) {
this.statuses.clear();
this.statuses.addAll(statuses);
notifyDataSetChanged();
}
public void addItem(int position, StatusViewData.Concrete statusViewData) {
statuses.add(position, statusViewData);
notifyItemInserted(position);
}
public void clearItems() {
int oldSize = statuses.size();
statuses.clear();
detailedStatusPosition = RecyclerView.NO_POSITION;
notifyItemRangeRemoved(0, oldSize);
}
public void addAll(int position, List<StatusViewData.Concrete> statuses) {
this.statuses.addAll(position, statuses);
notifyItemRangeInserted(position, statuses.size());
}
public void addAll(List<StatusViewData.Concrete> statuses) {
int end = statuses.size();
this.statuses.addAll(statuses);
notifyItemRangeInserted(end, statuses.size());
}
public void removeItem(int position) {
statuses.remove(position);
notifyItemRemoved(position);
}
public void clear() {
statuses.clear();
detailedStatusPosition = RecyclerView.NO_POSITION;
notifyDataSetChanged();
}
public void setItem(int position, StatusViewData.Concrete status, boolean notifyAdapter) {
statuses.set(position, status);
if (notifyAdapter) {
notifyItemChanged(position);
}
}
@Nullable
public StatusViewData.Concrete getItem(int position) {
if (position >= 0 && position < statuses.size()) {
return statuses.get(position);
} else {
return null;
}
}
public void setDetailedStatusPosition(int position) {
if (position != detailedStatusPosition
&& detailedStatusPosition != RecyclerView.NO_POSITION) {
int prior = detailedStatusPosition;
detailedStatusPosition = position;
notifyItemChanged(prior);
} else {
detailedStatusPosition = position;
}
}
public int getDetailedStatusPosition() {
return detailedStatusPosition;
}
}

View File

@ -0,0 +1,129 @@
/* Copyright 2021 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
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
class ThreadAdapter(
private val statusDisplayOptions: StatusDisplayOptions,
private val statusActionListener: StatusActionListener
) : RecyclerView.Adapter<StatusBaseViewHolder>() {
private val statuses = mutableListOf<StatusViewData.Concrete>()
var detailedStatusPosition: Int = RecyclerView.NO_POSITION
private set
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatusBaseViewHolder {
return when (viewType) {
VIEW_TYPE_STATUS -> {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_status, parent, false)
StatusViewHolder(view)
}
VIEW_TYPE_STATUS_DETAILED -> {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_status_detailed, parent, false)
StatusDetailedViewHolder(view)
}
else -> error("Unknown item type: $viewType")
}
}
override fun onBindViewHolder(viewHolder: StatusBaseViewHolder, position: Int) {
val status = statuses[position]
viewHolder.setupWithStatus(status, statusActionListener, statusDisplayOptions)
}
override fun getItemViewType(position: Int): Int {
return if (position == detailedStatusPosition) {
VIEW_TYPE_STATUS_DETAILED
} else {
VIEW_TYPE_STATUS
}
}
override fun getItemCount(): Int = statuses.size
fun setStatuses(statuses: List<StatusViewData.Concrete>?) {
this.statuses.clear()
this.statuses.addAll(statuses!!)
notifyDataSetChanged()
}
fun addItem(position: Int, statusViewData: StatusViewData.Concrete) {
statuses.add(position, statusViewData)
notifyItemInserted(position)
}
fun clearItems() {
val oldSize = statuses.size
statuses.clear()
detailedStatusPosition = RecyclerView.NO_POSITION
notifyItemRangeRemoved(0, oldSize)
}
fun addAll(position: Int, statuses: List<StatusViewData.Concrete>) {
this.statuses.addAll(position, statuses)
notifyItemRangeInserted(position, statuses.size)
}
fun addAll(statuses: List<StatusViewData.Concrete>) {
val end = statuses.size
this.statuses.addAll(statuses)
notifyItemRangeInserted(end, statuses.size)
}
fun removeItem(position: Int) {
statuses.removeAt(position)
notifyItemRemoved(position)
}
fun clear() {
statuses.clear()
detailedStatusPosition = RecyclerView.NO_POSITION
notifyDataSetChanged()
}
fun setItem(position: Int, status: StatusViewData.Concrete, notifyAdapter: Boolean) {
statuses[position] = status
if (notifyAdapter) {
notifyItemChanged(position)
}
}
fun getItem(position: Int): StatusViewData.Concrete? = statuses.getOrNull(position)
fun setDetailedStatusPosition(position: Int) {
if (position != detailedStatusPosition
&& detailedStatusPosition != RecyclerView.NO_POSITION
) {
val prior = detailedStatusPosition
detailedStatusPosition = position
notifyItemChanged(prior)
} else {
detailedStatusPosition = position
}
}
companion object {
private const val VIEW_TYPE_STATUS = 0
private const val VIEW_TYPE_STATUS_DETAILED = 1
}
}

View File

@ -67,7 +67,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
private var id: String? = null
private lateinit var scrollListener: EndlessOnScrollListener
private lateinit var adapter: AccountAdapter
private lateinit var adapter: AccountAdapter<*>
private var fetching = false
private var bottomId: String? = null