diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/api/Connection.java b/app/src/main/java/org/nuclearfog/twidda/backend/api/Connection.java index 86fce7f4..1e684f68 100644 --- a/app/src/main/java/org/nuclearfog/twidda/backend/api/Connection.java +++ b/app/src/main/java/org/nuclearfog/twidda/backend/api/Connection.java @@ -2,6 +2,7 @@ package org.nuclearfog.twidda.backend.api; import org.nuclearfog.twidda.backend.helper.ConnectionConfig; import org.nuclearfog.twidda.backend.helper.MediaStatus; +import org.nuclearfog.twidda.lists.Domains; import org.nuclearfog.twidda.lists.Messages; import org.nuclearfog.twidda.backend.helper.update.ProfileUpdate; import org.nuclearfog.twidda.backend.helper.update.StatusUpdate; @@ -448,6 +449,28 @@ public interface Connection { */ Status uploadStatus(StatusUpdate update, List mediaIds) throws ConnectionException; + /** + * return a list of domain names the current user has blocked + * + * @param cursor cursor to parse the pages or 0L if not defined + * @return domain list + */ + Domains getDomainBlocks(long cursor) throws ConnectionException; + + /** + * block specific domain name + * + * @param domain domain name (without "https://") + */ + void blockDomain(String domain) throws ConnectionException; + + /** + * remove block of a specific domain name + * + * @param domain domain name (without "https://") + */ + void unblockDomain(String domain) throws ConnectionException; + /** * create userlist * diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/Mastodon.java b/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/Mastodon.java index 73b79e1b..161c0554 100644 --- a/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/Mastodon.java +++ b/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/Mastodon.java @@ -24,6 +24,7 @@ import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonTrend; import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonUser; import org.nuclearfog.twidda.backend.helper.ConnectionConfig; import org.nuclearfog.twidda.backend.helper.MediaStatus; +import org.nuclearfog.twidda.lists.Domains; import org.nuclearfog.twidda.lists.Messages; import org.nuclearfog.twidda.backend.helper.update.PollUpdate; import org.nuclearfog.twidda.backend.helper.update.ProfileUpdate; @@ -124,6 +125,7 @@ public class Mastodon implements Connection { private static final String ENDPOINT_PUBLIC_TIMELINE = "/api/v1/timelines/public"; private static final String ENDPOINT_CUSTOM_EMOJIS = "/api/v1/custom_emojis"; private static final String ENDPOINT_POLL = "/api/v1/polls/"; + private static final String ENDPOINT_DOMAIN_BLOCK = "/api/v1/domain_blocks"; private static final MediaType TYPE_TEXT = MediaType.parse("text/plain"); private static final MediaType TYPE_STREAM = MediaType.parse("application/octet-stream"); @@ -663,6 +665,61 @@ public class Mastodon implements Connection { } + @Override + public Domains getDomainBlocks(long cursor) throws ConnectionException { + try { + List params = new ArrayList<>(); + params.add("limit=" + settings.getListSize()); + params.add("maxId=" + cursor); + Response response = get(ENDPOINT_DOMAIN_BLOCK, params); + ResponseBody body = response.body(); + if (response.code() == 200 && body != null) { + JSONArray array = new JSONArray(body.string()); + long[] cursors = getCursors(response); + Domains result = new Domains(cursors[0], cursors[1]); + for (int i = 0 ; i < array.length() ; i++) { + result.add(array.getString(i)); + } + return result; + } + throw new MastodonException(response); + + } catch (JSONException | IOException e) { + throw new MastodonException(e); + } + } + + + @Override + public void blockDomain(String domain) throws ConnectionException { + try { + List params = new ArrayList<>(); + params.add("domain=" + domain); + Response response = post(ENDPOINT_DOMAIN_BLOCK, params); + if (response.code() != 200) { + throw new MastodonException(response); + } + } catch (IOException e) { + throw new MastodonException(e); + } + } + + + @Override + public void unblockDomain(String domain) throws ConnectionException { + try { + List params = new ArrayList<>(); + params.add("domain=" + domain); + Response response = delete(ENDPOINT_DOMAIN_BLOCK, params); + if (response.code() != 200) { + throw new MastodonException(response); + } + } catch (IOException e) { + throw new MastodonException(e); + } + } + + @Override public UserList createUserlist(UserListUpdate update) throws MastodonException { List params = new ArrayList<>(); diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/api/twitter/v1/TwitterV1.java b/app/src/main/java/org/nuclearfog/twidda/backend/api/twitter/v1/TwitterV1.java index 890c1ed2..4195fc2b 100644 --- a/app/src/main/java/org/nuclearfog/twidda/backend/api/twitter/v1/TwitterV1.java +++ b/app/src/main/java/org/nuclearfog/twidda/backend/api/twitter/v1/TwitterV1.java @@ -23,6 +23,7 @@ import org.nuclearfog.twidda.backend.api.twitter.v1.impl.UserListV1; import org.nuclearfog.twidda.backend.api.twitter.v1.impl.UserV1; import org.nuclearfog.twidda.backend.helper.ConnectionConfig; import org.nuclearfog.twidda.backend.helper.MediaStatus; +import org.nuclearfog.twidda.lists.Domains; import org.nuclearfog.twidda.lists.Messages; import org.nuclearfog.twidda.backend.helper.update.ProfileUpdate; import org.nuclearfog.twidda.backend.helper.update.StatusUpdate; @@ -764,6 +765,24 @@ public class TwitterV1 implements Connection { } + @Override + public Domains getDomainBlocks(long cursor) throws ConnectionException { + throw new TwitterException("not supported"); + } + + + @Override + public void blockDomain(String domain) throws ConnectionException { + throw new TwitterException("not supported"); + } + + + @Override + public void unblockDomain(String domain) throws ConnectionException { + throw new TwitterException("not supported"); + } + + @Override public UserList createUserlist(UserListUpdate update) throws TwitterException { List params = new ArrayList<>(); diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/async/DomainAction.java b/app/src/main/java/org/nuclearfog/twidda/backend/async/DomainAction.java new file mode 100644 index 00000000..d785f0a6 --- /dev/null +++ b/app/src/main/java/org/nuclearfog/twidda/backend/async/DomainAction.java @@ -0,0 +1,105 @@ +package org.nuclearfog.twidda.backend.async; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.nuclearfog.twidda.backend.api.Connection; +import org.nuclearfog.twidda.backend.api.ConnectionException; +import org.nuclearfog.twidda.backend.api.ConnectionManager; +import org.nuclearfog.twidda.lists.Domains; + +/** + * background executor to load/block domains + * + * @author nuclearfog + */ +public class DomainAction extends AsyncExecutor { + + private Connection connection; + + /** + * + */ + public DomainAction(Context context) { + connection = ConnectionManager.getDefaultConnection(context); + } + + + @Override + protected DomainResult doInBackground(@NonNull DomainParam param) { + try { + switch (param.mode) { + case DomainParam.MODE_LOAD: + Domains result = connection.getDomainBlocks(param.cursor); + return new DomainResult(DomainResult.MODE_LOAD, param.index, result, param.domain, null); + + case DomainParam.MODE_BLOCK: + connection.blockDomain(param.domain); + return new DomainResult(DomainResult.MODE_BLOCK, param.index, null, param.domain, null); + + case DomainParam.MODE_UNBLOCK: + connection.unblockDomain(param.domain); + return new DomainResult(DomainResult.MODE_UNBLOCK, param.index, null, param.domain, null); + } + } catch (ConnectionException exception) { + return new DomainResult(DomainResult.ERROR, param.index, null, param.domain, exception); + } catch (Exception exception) { + exception.printStackTrace(); + } + return new DomainResult(DomainResult.ERROR, param.index, null, param.domain, null); + } + + /** + * + */ + public static class DomainParam { + + public static final int MODE_LOAD = 1; + public static final int MODE_BLOCK = 2; + public static final int MODE_UNBLOCK = 3; + + public static final long NO_CURSOR = 0L; + + final String domain; + final long cursor; + final int mode; + final int index; + + public DomainParam(int mode, int index, long cursor, String domain) { + this.mode = mode; + this.cursor = cursor; + this.domain = domain; + this.index = index; + } + } + + /** + * + */ + public static class DomainResult { + + public static final int ERROR = -1; + public static final int MODE_LOAD = 4; + public static final int MODE_BLOCK = 5; + public static final int MODE_UNBLOCK = 6; + + public final int mode; + public final int index; + @Nullable + public final Domains domains; + @Nullable + public final ConnectionException exception; + @Nullable + public final String domain; + + DomainResult(int mode, int index, @Nullable Domains domains, @Nullable String domain, @Nullable ConnectionException exception) { + this.domains = domains; + this.domain = domain; + this.exception = exception; + this.mode = mode; + this.index = index; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/async/FilterLoader.java b/app/src/main/java/org/nuclearfog/twidda/backend/async/FilterLoader.java index 9862d4f8..91932bc4 100644 --- a/app/src/main/java/org/nuclearfog/twidda/backend/async/FilterLoader.java +++ b/app/src/main/java/org/nuclearfog/twidda/backend/async/FilterLoader.java @@ -44,15 +44,19 @@ public class FilterLoader extends AsyncExecutor { + + private static final long serialVersionUID = 7642308259992697427L; + + private long prevCursor, nextCursor; + + /** + * + */ + public Domains() { + this(0L, 0L); + } + + /** + * @param prevCursor cursor to the previous page + * @param nextCursor cursor to the next page + */ + public Domains(long prevCursor, long nextCursor) { + super(); + this.prevCursor = prevCursor; + this.nextCursor = nextCursor; + } + + /** + * @param domains list to clone + */ + public Domains(Domains domains) { + super(domains); + prevCursor = domains.prevCursor; + nextCursor = domains.nextCursor; + } + + /** + * clone existing list + * + * @param domains list to clone + */ + public void replaceAll(Domains domains) { + clear(); + addAll(domains); + prevCursor = domains.prevCursor; + nextCursor = domains.nextCursor; + } + + /** + * add new items at specific index + * + * @param index index where to insert new items + * @param list items to add + */ + public void addAll(int index, Domains list) { + if (isEmpty()) { + prevCursor = list.prevCursor; + nextCursor = list.nextCursor; + } else if (index == 0) { + prevCursor = list.prevCursor; + } else if (index == size() - 1) { + nextCursor = list.nextCursor; + } + super.addAll(index, list); + } + + /** + * get cursor for next items + * + * @return cursor or 0L if not set + */ + public long getNextCursor() { + return nextCursor; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/nuclearfog/twidda/ui/activities/ProfileActivity.java b/app/src/main/java/org/nuclearfog/twidda/ui/activities/ProfileActivity.java index 86e3070d..405db35a 100644 --- a/app/src/main/java/org/nuclearfog/twidda/ui/activities/ProfileActivity.java +++ b/app/src/main/java/org/nuclearfog/twidda/ui/activities/ProfileActivity.java @@ -42,6 +42,9 @@ import org.nuclearfog.textviewtool.LinkAndScrollMovement; import org.nuclearfog.twidda.R; import org.nuclearfog.twidda.backend.api.ConnectionException; import org.nuclearfog.twidda.backend.async.AsyncExecutor.AsyncCallback; +import org.nuclearfog.twidda.backend.async.DomainAction; +import org.nuclearfog.twidda.backend.async.DomainAction.DomainParam; +import org.nuclearfog.twidda.backend.async.DomainAction.DomainResult; import org.nuclearfog.twidda.backend.async.RelationLoader; import org.nuclearfog.twidda.backend.async.RelationLoader.RelationParam; import org.nuclearfog.twidda.backend.async.RelationLoader.RelationResult; @@ -121,6 +124,7 @@ public class ProfileActivity extends AppCompatActivity implements ActivityResult private static final int SCROLL_THRESHOLD = 10; private ActivityResultLauncher activityResultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), this); + private AsyncCallback domainCallback = this::setDomainResult; private AsyncCallback relationCallback = this::setRelationResult; private AsyncCallback userCallback = this::setUserResult; private AsyncCallback usernameUpdate = this::onUsernameUpdate; @@ -131,6 +135,7 @@ public class ProfileActivity extends AppCompatActivity implements ActivityResult private Picasso picasso; private ConfirmDialog confirmDialog; + private DomainAction domainAction; private RelationLoader relationLoader; private UserLoader userLoader; private TextEmojiLoader emojiLoader; @@ -181,6 +186,7 @@ public class ProfileActivity extends AppCompatActivity implements ActivityResult viewPager = findViewById(R.id.profile_pager); relationLoader = new RelationLoader(this); + domainAction = new DomainAction(this); userLoader = new UserLoader(this); emojiLoader = new TextEmojiLoader(this); picasso = PicassoBuilder.get(this); @@ -323,6 +329,7 @@ public class ProfileActivity extends AppCompatActivity implements ActivityResult boolean result = super.onPrepareOptionsMenu(m); if (user != null) { MenuItem listItem = m.findItem(R.id.profile_lists); + MenuItem domainBlock = m.findItem(R.id.profile_block_domain); switch (settings.getLogin().getConfiguration()) { case TWITTER1: @@ -339,6 +346,8 @@ public class ProfileActivity extends AppCompatActivity implements ActivityResult case MASTODON: if (user.isCurrentUser()) { listItem.setVisible(true); + } else { + domainBlock.setVisible(true); } break; } @@ -472,6 +481,12 @@ public class ProfileActivity extends AppCompatActivity implements ActivityResult startActivity(usersIntent); return true; } + // block user domain + else if (item.getItemId() == R.id.profile_block_domain) { + if (user != null) { + confirmDialog.show(ConfirmDialog.DOMAIN_BLOCK_ADD); + } + } return super.onOptionsItemSelected(item); } @@ -594,6 +609,12 @@ public class ProfileActivity extends AppCompatActivity implements ActivityResult RelationParam param = new RelationParam(user.getId(), RelationParam.MUTE); relationLoader.execute(param, relationCallback); } + // confirmed domain block + else if (type == ConfirmDialog.DOMAIN_BLOCK_ADD) { + String url = Uri.parse(user.getProfileUrl()).getHost(); + DomainParam param = new DomainParam(DomainParam.MODE_BLOCK, 0, DomainParam.NO_CURSOR, url); + domainAction.execute(param, domainCallback); + } } } @@ -675,7 +696,7 @@ public class ProfileActivity extends AppCompatActivity implements ActivityResult private void setRelationResult(@NonNull RelationResult result) { switch (result.mode) { case RelationResult.BLOCK: - Toast.makeText(getApplicationContext(), R.string.info_user_blocked, Toast.LENGTH_SHORT).show(); + Toast.makeText(getApplicationContext(), R.string.info_blocked, Toast.LENGTH_SHORT).show(); break; case RelationResult.UNBLOCK: @@ -709,6 +730,18 @@ public class ProfileActivity extends AppCompatActivity implements ActivityResult } } + /** + * set domain block result + */ + private void setDomainResult(DomainResult result) { + if (result.mode == DomainResult.MODE_BLOCK) { + Toast.makeText(getApplicationContext(), R.string.info_domain_blocked, Toast.LENGTH_SHORT).show(); + } else if (result.mode == DomainResult.ERROR) { + String message = ErrorHandler.getErrorMessage(this, result.exception); + Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT).show(); + } + } + /** * Set User Information * diff --git a/app/src/main/java/org/nuclearfog/twidda/ui/activities/UserlistActivity.java b/app/src/main/java/org/nuclearfog/twidda/ui/activities/UserlistActivity.java index af2bbcde..765acd6a 100644 --- a/app/src/main/java/org/nuclearfog/twidda/ui/activities/UserlistActivity.java +++ b/app/src/main/java/org/nuclearfog/twidda/ui/activities/UserlistActivity.java @@ -19,7 +19,6 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.SearchView; import androidx.appcompat.widget.SearchView.OnQueryTextListener; import androidx.appcompat.widget.Toolbar; -import androidx.fragment.app.Fragment; import androidx.viewpager2.widget.ViewPager2; import org.nuclearfog.twidda.R; @@ -34,12 +33,10 @@ import org.nuclearfog.twidda.backend.async.UserlistManager.ListManagerResult; import org.nuclearfog.twidda.backend.utils.AppStyles; import org.nuclearfog.twidda.backend.utils.ErrorHandler; import org.nuclearfog.twidda.config.GlobalSettings; -import org.nuclearfog.twidda.model.User; import org.nuclearfog.twidda.model.UserList; import org.nuclearfog.twidda.ui.adapter.FragmentAdapter; import org.nuclearfog.twidda.ui.dialogs.ConfirmDialog; import org.nuclearfog.twidda.ui.dialogs.ConfirmDialog.OnConfirmListener; -import org.nuclearfog.twidda.ui.fragments.UserFragment; import org.nuclearfog.twidda.ui.views.TabSelector; import org.nuclearfog.twidda.ui.views.TabSelector.OnTabSelectedListener; @@ -100,8 +97,6 @@ public class UserlistActivity extends AppCompatActivity implements ActivityResul @Nullable private UserList userList; - @Nullable - private User user; @Override @@ -283,13 +278,6 @@ public class UserlistActivity extends AppCompatActivity implements ActivityResul listLoaderAsync.execute(param, userlistSet); } } - // remove user from list - else if (type == ConfirmDialog.LIST_REMOVE_USER) { - if (listManagerAsync.isIdle() && userList != null && user != null) { - ListManagerParam param = new ListManagerParam(ListManagerParam.REMOVE, userList.getId(), user.getScreenname()); - listManagerAsync.execute(param, userlistUpdate); - } - } } @@ -322,19 +310,6 @@ public class UserlistActivity extends AppCompatActivity implements ActivityResul return false; } - - /** - * called from {@link org.nuclearfog.twidda.ui.fragments.UserFragment} when an user should be removed from a list - * - * @param user user to remove from the lsit - */ - public void onDelete(User user) { - if (!confirmDialog.isShowing()) { - confirmDialog.show(ConfirmDialog.LIST_REMOVE_USER); - this.user = user; - } - } - /** * update userlist member */ @@ -351,19 +326,6 @@ public class UserlistActivity extends AppCompatActivity implements ActivityResul invalidateOptionsMenu(); break; - case ListManagerResult.DEL_USER: - if (user != null) { - info = getString(R.string.info_user_removed, user.getScreenname()); - Toast.makeText(getApplicationContext(), info, Toast.LENGTH_SHORT).show(); - // remove user from list member page - Fragment fragment = adapter.getItem(1); - if (fragment instanceof UserFragment) { - UserFragment callback = (UserFragment) fragment; - callback.removeUser(user); - } - } - break; - case ListManagerResult.ERROR: String message = ErrorHandler.getErrorMessage(this, result.exception); Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT).show(); diff --git a/app/src/main/java/org/nuclearfog/twidda/ui/activities/UsersActivity.java b/app/src/main/java/org/nuclearfog/twidda/ui/activities/UsersActivity.java index 04d754f9..bf5a3107 100644 --- a/app/src/main/java/org/nuclearfog/twidda/ui/activities/UsersActivity.java +++ b/app/src/main/java/org/nuclearfog/twidda/ui/activities/UsersActivity.java @@ -2,7 +2,9 @@ package org.nuclearfog.twidda.ui.activities; import android.content.Context; import android.graphics.Color; +import android.net.Uri; import android.os.Bundle; +import android.util.Patterns; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -170,11 +172,17 @@ public class UsersActivity extends AppCompatActivity implements OnTabSelectedLis break; case USERS_EXCLUDED: - adapter.setupMuteBlockPage(); - viewPager.setOffscreenPageLimit(2); + if (settings.getLogin().getConfiguration() == Configuration.MASTODON) { + adapter.setupBlockPage(true); + viewPager.setOffscreenPageLimit(3); + tabSelector.addTabIcons(R.array.user_domain_exclude_icons); + } else { + adapter.setupBlockPage(false); + viewPager.setOffscreenPageLimit(2); + tabSelector.addTabIcons(R.array.user_exclude_icons); + } tabSelector.addViewPager(viewPager); tabSelector.addOnTabSelectedListener(this); - tabSelector.addTabIcons(R.array.user_exclude_icons); toolbar.setTitle(R.string.menu_toolbar_excluded_users); break; @@ -230,6 +238,9 @@ public class UsersActivity extends AppCompatActivity implements OnTabSelectedLis } else if (viewPager.getCurrentItem() == 1) { String hint = getString(R.string.menu_hint_block_user); searchView.setQueryHint(hint); + } else if (viewPager.getCurrentItem() == 2) { + String hint = getString(R.string.menu_hint_block_domain); + searchView.setQueryHint(hint); } return true; } @@ -261,19 +272,33 @@ public class UsersActivity extends AppCompatActivity implements OnTabSelectedLis @Override public boolean onQueryTextSubmit(String query) { - if (USERNAME_PATTERN.matcher(query).matches()) { - if (filterLoader.isIdle()) { - if (viewPager.getCurrentItem() == 0) { - FilterParam param = new FilterParam(FilterParam.MUTE, query); - filterLoader.execute(param, this); - } else if (viewPager.getCurrentItem() == 1) { - FilterParam param = new FilterParam(FilterParam.BLOCK, query); - filterLoader.execute(param, this); - } + if (!filterLoader.isIdle()) + return false; + if (viewPager.getCurrentItem() == 0) { + if (USERNAME_PATTERN.matcher(query).matches()) { + FilterParam param = new FilterParam(FilterParam.MUTE_USER, query); + filterLoader.execute(param, this); return true; } - } else { Toast.makeText(getApplicationContext(), R.string.error_username_format, Toast.LENGTH_SHORT).show(); + } else if (viewPager.getCurrentItem() == 1) { + if (USERNAME_PATTERN.matcher(query).matches()) { + FilterParam param = new FilterParam(FilterParam.BLOCK_USER, query); + filterLoader.execute(param, this); + return true; + } + Toast.makeText(getApplicationContext(), R.string.error_username_format, Toast.LENGTH_SHORT).show(); + } else if (viewPager.getCurrentItem() == 2) { + if (Patterns.WEB_URL.matcher(query).matches()) { + FilterParam param; + if (query.startsWith("https://")) + param = new FilterParam(FilterParam.BLOCK_DOMAIN, Uri.parse(query).getHost()); + else + param = new FilterParam(FilterParam.BLOCK_DOMAIN, query); + filterLoader.execute(param, this); + return true; + } + Toast.makeText(getApplicationContext(), R.string.error_domain_format, Toast.LENGTH_SHORT).show(); } return false; } @@ -288,13 +313,14 @@ public class UsersActivity extends AppCompatActivity implements OnTabSelectedLis @Override public void onResult(@NonNull FilterResult result) { switch (result.mode) { - case FilterResult.MUTE: + case FilterResult.MUTE_USER: Toast.makeText(getApplicationContext(), R.string.info_user_muted, Toast.LENGTH_SHORT).show(); invalidateOptionsMenu(); break; - case FilterResult.BLOCK: - Toast.makeText(getApplicationContext(), R.string.info_user_blocked, Toast.LENGTH_SHORT).show(); + case FilterResult.BLOCK_DOMAIN: + case FilterResult.BLOCK_USER: + Toast.makeText(getApplicationContext(), R.string.info_blocked, Toast.LENGTH_SHORT).show(); invalidateOptionsMenu(); break; diff --git a/app/src/main/java/org/nuclearfog/twidda/ui/adapter/DomainAdapter.java b/app/src/main/java/org/nuclearfog/twidda/ui/adapter/DomainAdapter.java new file mode 100644 index 00000000..0245a67b --- /dev/null +++ b/app/src/main/java/org/nuclearfog/twidda/ui/adapter/DomainAdapter.java @@ -0,0 +1,190 @@ +package org.nuclearfog.twidda.ui.adapter; + +import android.content.Context; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView.Adapter; +import androidx.recyclerview.widget.RecyclerView.ViewHolder; + +import org.nuclearfog.twidda.config.GlobalSettings; +import org.nuclearfog.twidda.lists.Domains; +import org.nuclearfog.twidda.ui.adapter.holder.DomainHolder; +import org.nuclearfog.twidda.ui.adapter.holder.OnHolderClickListener; +import org.nuclearfog.twidda.ui.adapter.holder.PlaceHolder; + +/** + * RecyclerView adapter for domain list + * + * @author nuclearfog + */ +public class DomainAdapter extends Adapter implements OnHolderClickListener { + + private static final int TYPE_ITEM = 0; + private static final int TYPE_FOOTER = 1; + + private static final int NO_LOADING = -1; + public static final int NO_INDEX = -1; + + private OnDomainClickListener listener; + private GlobalSettings settings; + + private Domains items = new Domains(); + private int loadingIndex = NO_LOADING; + + /** + * + */ + public DomainAdapter(Context context, OnDomainClickListener listener) { + settings = GlobalSettings.getInstance(context); + this.listener = listener; + } + + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + if (viewType == TYPE_ITEM) { + return new DomainHolder(parent, settings, this); + } else { + return new PlaceHolder(parent, settings, false, this); + } + } + + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + if (holder instanceof DomainHolder) { + DomainHolder domainHolder = (DomainHolder) holder; + domainHolder.setDomain(items.get(position)); + } else if (holder instanceof PlaceHolder) { + PlaceHolder placeHolder = (PlaceHolder) holder; + placeHolder.setLoading(loadingIndex == position); + } + } + + + @Override + public int getItemCount() { + return items.size(); + } + + + @Override + public int getItemViewType(int position) { + if (items.get(position) != null) + return TYPE_ITEM; + return TYPE_FOOTER; + } + + + @Override + public void onItemClick(int position, int type, int... extras) { + if (type == DOMAIN_REMOVE) { + listener.onDomainRemove(items.get(position)); + } + } + + + @Override + public boolean onPlaceholderClick(int index) { + return listener.onPlaceholderClick(index, items.getNextCursor()); + } + + /** + * replace old items with new items + * + * @param domains new items + */ + public void replaceItems(Domains domains) { + disableLoading(); + items.replaceAll(domains); + if (items.getNextCursor() != 0L && items.peekLast() != null) { + items.add(null); + } + notifyDataSetChanged(); + } + + /** + * insert new items at specific position + * + * @param domains new items + * @param index index where to insert new items + */ + public void addItems(Domains domains, int index) { + disableLoading(); + if (index < 0) { + items.replaceAll(domains); + if (items.getNextCursor() != 0L) { + items.add(null); + } + notifyDataSetChanged(); + } else { + items.addAll(index, domains); + if (items.getNextCursor() != 0L && items.peekLast() != null) { + items.add(null); + notifyItemRangeInserted(index, domains.size() + 1); + } else if (items.getNextCursor() == 0L && items.peekLast() == null) { + items.pollLast(); + notifyItemRangeInserted(index, domains.size() - 1); + } else if (!domains.isEmpty()) { + notifyItemRangeInserted(index, domains.size()); + } + } + } + + /** + * get all items used in this adapter + * + * @return domain list + */ + public Domains getItems() { + return new Domains(items); + } + + /** + * remove specific item from list + * + * @param item domain name item + */ + public void removeItem(String item) { + int index = items.indexOf(item); + if (index >= 0) { + items.remove(index); + notifyItemRemoved(index); + } + } + + /** + * disable placeholder view loading animation + */ + public void disableLoading() { + if (loadingIndex != NO_LOADING) { + int oldIndex = loadingIndex; + loadingIndex = NO_LOADING; + notifyItemChanged(oldIndex); + } + } + + /** + * listener for list item + */ + public interface OnDomainClickListener { + + /** + * called on remove button click + * + * @param domain item of selected item + */ + void onDomainRemove(String domain); + + /** + * called on footer click + * + * @param index index of the footer + * @param cursor cursor for the next items + * @return true if click was performed + */ + boolean onPlaceholderClick(int index, long cursor); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/nuclearfog/twidda/ui/adapter/FragmentAdapter.java b/app/src/main/java/org/nuclearfog/twidda/ui/adapter/FragmentAdapter.java index 616ad643..e4a90087 100644 --- a/app/src/main/java/org/nuclearfog/twidda/ui/adapter/FragmentAdapter.java +++ b/app/src/main/java/org/nuclearfog/twidda/ui/adapter/FragmentAdapter.java @@ -9,6 +9,7 @@ import androidx.viewpager2.adapter.FragmentStateAdapter; import org.nuclearfog.twidda.config.GlobalSettings; import org.nuclearfog.twidda.model.Account; +import org.nuclearfog.twidda.ui.fragments.DomainFragment; import org.nuclearfog.twidda.ui.fragments.ListFragment; import org.nuclearfog.twidda.ui.fragments.MessageFragment; import org.nuclearfog.twidda.ui.fragments.NotificationFragment; @@ -64,19 +65,6 @@ public class FragmentAdapter extends FragmentStateAdapter { return fragments.length; } - /** - * get fragment at index - * - * @param index index of the fragment - * @return fragment - */ - public ListFragment getItem(int index) { - if (index >= 0 && index < fragments.length) { - return fragments[index]; - } - return null; - } - /** * Check if adapter is empty * @@ -297,12 +285,17 @@ public class FragmentAdapter extends FragmentStateAdapter { /** * setup adapter for a page of muted / blocked users */ - public void setupMuteBlockPage() { + public void setupBlockPage(boolean enableDomainBlock) { Bundle paramMuteList = new Bundle(); Bundle paramBlockList = new Bundle(); paramMuteList.putInt(UserFragment.KEY_MODE, UserFragment.MODE_MUTES); paramBlockList.putInt(UserFragment.KEY_MODE, UserFragment.MODE_BLOCKS); - fragments = new ListFragment[2]; + if (enableDomainBlock) { + fragments = new ListFragment[3]; + fragments[2] = new DomainFragment(); + } else { + fragments = new ListFragment[2]; + } fragments[0] = new UserFragment(); fragments[1] = new UserFragment(); fragments[0].setArguments(paramMuteList); diff --git a/app/src/main/java/org/nuclearfog/twidda/ui/adapter/holder/DomainHolder.java b/app/src/main/java/org/nuclearfog/twidda/ui/adapter/holder/DomainHolder.java new file mode 100644 index 00000000..48131fd4 --- /dev/null +++ b/app/src/main/java/org/nuclearfog/twidda/ui/adapter/holder/DomainHolder.java @@ -0,0 +1,62 @@ +package org.nuclearfog.twidda.ui.adapter.holder; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.cardview.widget.CardView; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView.ViewHolder; + +import org.nuclearfog.twidda.R; +import org.nuclearfog.twidda.config.GlobalSettings; + +/** + * domain holder class for {@link org.nuclearfog.twidda.ui.adapter.DomainAdapter} + * + * @author nuclearfog + */ +public class DomainHolder extends ViewHolder implements OnClickListener { + + private TextView domain_name; + + private OnHolderClickListener listener; + + /** + * + */ + public DomainHolder(ViewGroup parent, GlobalSettings settings, OnHolderClickListener listener) { + super(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_domain, parent, false)); + this.listener = listener; + CardView card = (CardView) itemView; + View deleteButton = itemView.findViewById(R.id.item_domain_delete); + domain_name = itemView.findViewById(R.id.item_domain_name); + domain_name.setTextColor(settings.getTextColor()); + domain_name.setTypeface(settings.getTypeFace()); + card.setCardBackgroundColor(settings.getCardColor()); + + deleteButton.setOnClickListener(this); + } + + + @Override + public void onClick(View v) { + if (v.getId() == R.id.item_domain_delete) { + int position = getLayoutPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onItemClick(position, OnHolderClickListener.DOMAIN_REMOVE); + } + } + } + + /** + * set domain information + * + * @param name domain information + */ + public void setDomain(String name) { + domain_name.setText(name); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/nuclearfog/twidda/ui/adapter/holder/OnHolderClickListener.java b/app/src/main/java/org/nuclearfog/twidda/ui/adapter/holder/OnHolderClickListener.java index badccb3e..df263396 100644 --- a/app/src/main/java/org/nuclearfog/twidda/ui/adapter/holder/OnHolderClickListener.java +++ b/app/src/main/java/org/nuclearfog/twidda/ui/adapter/holder/OnHolderClickListener.java @@ -49,6 +49,8 @@ public interface OnHolderClickListener { int EMOJI_CLICK = 20; + int DOMAIN_REMOVE = 21; + /** * called when an item was clicked * diff --git a/app/src/main/java/org/nuclearfog/twidda/ui/dialogs/ConfirmDialog.java b/app/src/main/java/org/nuclearfog/twidda/ui/dialogs/ConfirmDialog.java index 7f9f58b2..1b8dc996 100644 --- a/app/src/main/java/org/nuclearfog/twidda/ui/dialogs/ConfirmDialog.java +++ b/app/src/main/java/org/nuclearfog/twidda/ui/dialogs/ConfirmDialog.java @@ -127,6 +127,10 @@ public class ConfirmDialog extends Dialog implements OnClickListener { */ public static final int NOTIFICATION_DISMISS = 623; + public static final int DOMAIN_BLOCK_ADD = 624; + + public static final int DOMAIN_BLOCK_REMOVE = 625; + private TextView title, message; private Button confirm, cancel; @@ -236,6 +240,14 @@ public class ConfirmDialog extends Dialog implements OnClickListener { messageRes = R.string.confirm_dismiss_notification; break; + case DOMAIN_BLOCK_ADD: + messageRes = R.string.confirm_add_domain_block; + break; + + case DOMAIN_BLOCK_REMOVE: + messageRes = R.string.confirm_remove_domain_block; + break; + case PROFILE_UNFOLLOW: messageRes = R.string.confirm_unfollow; break; diff --git a/app/src/main/java/org/nuclearfog/twidda/ui/fragments/DomainFragment.java b/app/src/main/java/org/nuclearfog/twidda/ui/fragments/DomainFragment.java new file mode 100644 index 00000000..51aa200d --- /dev/null +++ b/app/src/main/java/org/nuclearfog/twidda/ui/fragments/DomainFragment.java @@ -0,0 +1,139 @@ +package org.nuclearfog.twidda.ui.fragments; + +import android.os.Bundle; +import android.view.View; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.nuclearfog.twidda.R; +import org.nuclearfog.twidda.backend.async.AsyncExecutor.AsyncCallback; +import org.nuclearfog.twidda.backend.async.DomainAction; +import org.nuclearfog.twidda.backend.async.DomainAction.DomainParam; +import org.nuclearfog.twidda.backend.async.DomainAction.DomainResult; +import org.nuclearfog.twidda.lists.Domains; +import org.nuclearfog.twidda.ui.adapter.DomainAdapter; +import org.nuclearfog.twidda.ui.adapter.DomainAdapter.OnDomainClickListener; +import org.nuclearfog.twidda.ui.dialogs.ConfirmDialog; +import org.nuclearfog.twidda.ui.dialogs.ConfirmDialog.OnConfirmListener; + +import java.io.Serializable; + +/** + * Fragment containing a list of domain names + * + * @author nuclearfog + */ +public class DomainFragment extends ListFragment implements OnDomainClickListener, OnConfirmListener, AsyncCallback { + + private static final String KEY_DATA = "domain-data"; + + private DomainAction domainAction; + private DomainAdapter adapter; + private ConfirmDialog dialog; + + private String selectedDomain = ""; + + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + adapter = new DomainAdapter(requireContext(), this); + domainAction = new DomainAction(requireContext()); + dialog = new ConfirmDialog(requireContext()); + setAdapter(adapter); + dialog.setConfirmListener(this); + + if (savedInstanceState != null) { + Serializable data = savedInstanceState.getSerializable(KEY_DATA); + if (data instanceof Domains) { + adapter.replaceItems((Domains) data); + return; + } + } + load(DomainAdapter.NO_INDEX, DomainParam.NO_CURSOR); + setRefresh(true); + } + + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + outState.putSerializable(KEY_DATA, adapter.getItems()); + super.onSaveInstanceState(outState); + } + + + @Override + public void onDestroy() { + domainAction.cancel(); + super.onDestroy(); + } + + + @Override + protected void onReload() { + load(DomainAdapter.NO_INDEX, DomainParam.NO_CURSOR); + } + + + @Override + protected void onReset() { + setRefresh(true); + load(DomainAdapter.NO_INDEX, DomainParam.NO_CURSOR); + } + + + @Override + public void onDomainRemove(String domain) { + if (!isRefreshing() && !dialog.isShowing()) { + dialog.show(ConfirmDialog.DOMAIN_BLOCK_REMOVE); + selectedDomain = domain; + } + } + + + @Override + public boolean onPlaceholderClick(int index, long cursor) { + if (!isRefreshing()) { + load(index, cursor); + return true; + } + return false; + } + + + @Override + public void onResult(@NonNull DomainResult result) { + setRefresh(false); + if (result.mode == DomainResult.MODE_LOAD) { + if (result.domains != null) { + adapter.addItems(result.domains, result.index); + } + } else if (result.mode == DomainResult.MODE_UNBLOCK) { + if (result.domain != null) { + adapter.removeItem(result.domain); + Toast.makeText(requireContext(), R.string.info_domain_removed, Toast.LENGTH_SHORT).show(); + } + } + } + + + @Override + public void onConfirm(int type) { + if (type == ConfirmDialog.DOMAIN_BLOCK_REMOVE) { + DomainParam param = new DomainParam(DomainParam.MODE_UNBLOCK, DomainAdapter.NO_INDEX, DomainParam.NO_CURSOR, selectedDomain); + domainAction.execute(param, this); + } + } + + /** + * load domain list + * + * @param index index where to insert domains into the list + * @param cursor cursor used to page through results + */ + private void load(int index, long cursor) { + DomainParam param = new DomainParam(DomainParam.MODE_LOAD, index, cursor, null); + domainAction.execute(param, this); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/nuclearfog/twidda/ui/fragments/NotificationFragment.java b/app/src/main/java/org/nuclearfog/twidda/ui/fragments/NotificationFragment.java index 0faacc14..da1109e6 100644 --- a/app/src/main/java/org/nuclearfog/twidda/ui/fragments/NotificationFragment.java +++ b/app/src/main/java/org/nuclearfog/twidda/ui/fragments/NotificationFragment.java @@ -67,6 +67,7 @@ public class NotificationFragment extends ListFragment implements OnNotification notificationAction = new NotificationAction(requireContext()); adapter = new NotificationAdapter(requireContext(), this); setAdapter(adapter); + confirmDialog.setConfirmListener(this); if (savedInstanceState != null) { Serializable data = savedInstanceState.getSerializable(KEY_DATA); @@ -75,7 +76,6 @@ public class NotificationFragment extends ListFragment implements OnNotification return; } } - confirmDialog.setConfirmListener(this); load(0L, 0L, 0); setRefresh(true); } diff --git a/app/src/main/java/org/nuclearfog/twidda/ui/fragments/UserFragment.java b/app/src/main/java/org/nuclearfog/twidda/ui/fragments/UserFragment.java index 19c9e78e..ccbedf51 100644 --- a/app/src/main/java/org/nuclearfog/twidda/ui/fragments/UserFragment.java +++ b/app/src/main/java/org/nuclearfog/twidda/ui/fragments/UserFragment.java @@ -12,7 +12,10 @@ import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.nuclearfog.twidda.R; import org.nuclearfog.twidda.backend.async.AsyncExecutor.AsyncCallback; +import org.nuclearfog.twidda.backend.async.UserlistManager; +import org.nuclearfog.twidda.backend.async.UserlistManager.ListManagerResult; import org.nuclearfog.twidda.backend.async.UsersLoader; import org.nuclearfog.twidda.backend.async.UsersLoader.UserParam; import org.nuclearfog.twidda.backend.async.UsersLoader.UserResult; @@ -20,9 +23,10 @@ import org.nuclearfog.twidda.lists.Users; import org.nuclearfog.twidda.backend.utils.ErrorHandler; import org.nuclearfog.twidda.model.User; import org.nuclearfog.twidda.ui.activities.ProfileActivity; -import org.nuclearfog.twidda.ui.activities.UserlistActivity; import org.nuclearfog.twidda.ui.adapter.UserAdapter; import org.nuclearfog.twidda.ui.adapter.UserAdapter.UserClickListener; +import org.nuclearfog.twidda.ui.dialogs.ConfirmDialog; +import org.nuclearfog.twidda.ui.dialogs.ConfirmDialog.OnConfirmListener; import java.io.Serializable; @@ -31,7 +35,7 @@ import java.io.Serializable; * * @author nuclearfog */ -public class UserFragment extends ListFragment implements UserClickListener, AsyncCallback, ActivityResultCallback { +public class UserFragment extends ListFragment implements UserClickListener, OnConfirmListener, AsyncCallback, ActivityResultCallback { /** * key to set the type of user list to show @@ -145,10 +149,14 @@ public class UserFragment extends ListFragment implements UserClickListener, Asy private ActivityResultLauncher activityResultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), this); + private AsyncCallback userlistUpdate = this::updateUsers; + private ConfirmDialog confirmDialog; private UsersLoader userLoader; + private UserlistManager userlistManager; private UserAdapter adapter; + private User selectedUser; private String search = ""; private long id = 0; private int mode = 0; @@ -158,8 +166,11 @@ public class UserFragment extends ListFragment implements UserClickListener, Asy public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); userLoader = new UsersLoader(requireContext()); + userlistManager = new UserlistManager(requireContext()); + confirmDialog = new ConfirmDialog(requireContext()); adapter = new UserAdapter(requireContext(), this); setAdapter(adapter); + confirmDialog.setConfirmListener(this); Bundle param = getArguments(); if (param != null) { @@ -244,10 +255,9 @@ public class UserFragment extends ListFragment implements UserClickListener, Asy @Override public void onDelete(User user) { - if (getActivity() instanceof UserlistActivity) { - // call parent activity to handle user delete - UserlistActivity callback = (UserlistActivity) getActivity(); - callback.onDelete(user); + if (!confirmDialog.isShowing()) { + confirmDialog.show(ConfirmDialog.LIST_REMOVE_USER); + this.selectedUser = user; } } @@ -264,15 +274,30 @@ public class UserFragment extends ListFragment implements UserClickListener, Asy setRefresh(false); } - /** - * remove specific user from fragment list - * - * @param user user to remove - */ - public void removeUser(User user) { - adapter.removeItem(user); + + @Override + public void onConfirm(int type) { + // remove user from list + if (type == ConfirmDialog.LIST_REMOVE_USER) { + if (userlistManager.isIdle() && selectedUser != null) { + UserlistManager.ListManagerParam param = new UserlistManager.ListManagerParam(UserlistManager.ListManagerParam.REMOVE, id, selectedUser.getScreenname()); + userlistManager.execute(param, userlistUpdate); + } + } } + /** + * callback for userlist changes + */ + private void updateUsers(ListManagerResult result) { + if (result.mode == ListManagerResult.DEL_USER) { + if (selectedUser != null) { + String info = getString(R.string.info_user_removed, selectedUser.getScreenname()); + Toast.makeText(requireContext(), info, Toast.LENGTH_SHORT).show(); + adapter.removeItem(selectedUser); + } + } + } /** * load content into the list diff --git a/app/src/main/res/drawable/plus.xml b/app/src/main/res/drawable/plus.xml new file mode 100644 index 00000000..38df35c3 --- /dev/null +++ b/app/src/main/res/drawable/plus.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_domain.xml b/app/src/main/res/layout/item_domain.xml new file mode 100644 index 00000000..323c8664 --- /dev/null +++ b/app/src/main/res/layout/item_domain.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/excludelist.xml b/app/src/main/res/menu/excludelist.xml index d8bab61a..068e7902 100644 --- a/app/src/main/res/menu/excludelist.xml +++ b/app/src/main/res/menu/excludelist.xml @@ -5,7 +5,7 @@ diff --git a/app/src/main/res/menu/profile.xml b/app/src/main/res/menu/profile.xml index 067829ac..22f8bf3e 100644 --- a/app/src/main/res/menu/profile.xml +++ b/app/src/main/res/menu/profile.xml @@ -20,6 +20,11 @@ android:title="@string/menu_block_user" android:visible="false" /> + + Hintergrund Status löschen? PIN eingeben! + Nutzerdomain sperren? + Domain von der Liste entfernen? App Daten Löschen? "Anfragelimit erreicht! Entsperrung nach:" Status gesendet @@ -26,6 +28,7 @@ %1$s hat deinen Status geteilt Datenbank gelöscht Nachricht verworfen + Domain gesperrt! Fehler! bearbeiten löschen @@ -35,7 +38,7 @@ blocken entblocken gefolgt! - geblockt! + geblockt! entblockt! Status gelöscht! "gesendet von: " @@ -171,6 +174,7 @@ %1$s wurde zur Liste hinzugefügt! Nutzername falsch! Undefinierter Fehler! + Falsches Domainformat! Status ist nicht für Antworten freigegeben! Fehler, API Schlüssel ist fehlerhaft! Account Informationen konnten nicht aktualisiert werden! Bitte Eingaben überprüfen. @@ -200,6 +204,7 @@ \@name blockieren Nutzer ausschließen Ergebnisse filtern + domain.namen eingeben Filterliste aktualisieren aktualisiere Filterliste Filterliste aktualisiert! @@ -229,6 +234,7 @@ Antwort wurde ausgeblendet Antwort wurde eingeblendet öffne Twitter-Links mit Nitter + entferne Domain von der Liste Unbekannter Fehler! Ungültige Mediendatei! Anfragen @@ -265,6 +271,7 @@ Umfrage abgestimmt! Nachricht verwerfen? JSON Fehler! + Domain sperren Lesezeichen hinzufügen Lesezeichen entfernen Benachrichtigung verwerfen @@ -294,5 +301,6 @@ folge Hashtag entfolge Hashtag Hashtag entfolgt + Domain von der Liste entfernt Hashtag gefolgt \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 1c624484..893e8b2e 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -109,7 +109,7 @@ 个人资料已更新 已关注 已取消关注 - 已屏蔽 + 已屏蔽 已取消屏蔽 推文已删除 推文已发送 diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 6c0ff542..14882fc6 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -32,6 +32,12 @@ @drawable/block + + @drawable/mute + @drawable/block + @drawable/link + + @drawable/user @drawable/hash diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 2eda757b..5673b3a8 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -310,4 +310,10 @@ 5dp + + 8dp + 20sp + 36dp + 7dp + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6279fd8d..41fd58ec 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -41,7 +41,7 @@ Profile updated followed unfollowed - blocked + blocked unblocked Status removed Status shared @@ -66,7 +66,9 @@ Database cleared Notification dismissed hashtag unfollowed + domain removed from the list hashtag followed + domain blocked! Error @@ -97,6 +99,7 @@ Directmessage is too long! Empty list title! Wrong username format! + Wrong domain format! You can\'t reply to this status! Error, corrupt API key! Account update failed! Please check your input! @@ -129,7 +132,8 @@ Userlists About Licenses - block + block user + block domain bookmark remove bookmark unmute @@ -155,6 +159,7 @@ filter results enter @name to mute enter @name to block + enter domain.name refresh exclude list Logins add account @@ -205,6 +210,8 @@ clear app data delete status? dismiss notification? + block user domain? + remove domain from list? clear app data? "sent from: " Username @@ -289,6 +296,7 @@ update list remove user from list? remove user from list + remove domain from list unknown error! following list change profile image