added hashtag remove button, database update, translation update, bug fix

This commit is contained in:
nuclearfog 2023-09-27 21:42:01 +02:00
parent 774e3fa056
commit fb90a7d99e
No known key found for this signature in database
GPG Key ID: 03488A185C476379
22 changed files with 349 additions and 159 deletions

View File

@ -330,10 +330,10 @@ public interface Connection {
/**
*
* @param name name of the hashtag
* @param id of the featured hashtag
* @return updated hashtag information
*/
Hashtag unfeatureHashtag(String name) throws ConnectionException;
Hashtag unfeatureHashtag(long id) throws ConnectionException;
/**
* show current user's home timeline

View File

@ -541,13 +541,9 @@ public class Mastodon implements Connection {
@Override
public Hashtag unfeatureHashtag(String name) throws ConnectionException {
public Hashtag unfeatureHashtag(long id) throws ConnectionException {
try {
if (name.startsWith("#"))
name = name.substring(1);
List<String> params = new ArrayList<>();
params.add("name=" + StringUtils.encode(name));
return createTag(delete(ENDPOINT_HASHTAG_FEATURE, params));
return createTag(delete(ENDPOINT_HASHTAG_FEATURE + "/" + id, new ArrayList<>()));
} catch (IOException e) {
throw new MastodonException(e);
}
@ -1466,7 +1462,9 @@ public class Mastodon implements Connection {
long[] cursors = getCursors(response);
Trends result = new Trends(cursors[0], cursors[1]);
for (int i = 0; i < jsonArray.length(); i++) {
result.add(new MastodonHashtag(jsonArray.getJSONObject(i)));
MastodonHashtag item = new MastodonHashtag(jsonArray.getJSONObject(i));
item.setRank(i + 1);
result.add(item);
}
Collections.sort(result);
return result;

View File

@ -5,8 +5,8 @@ import androidx.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONObject;
import org.nuclearfog.twidda.model.Location;
import org.nuclearfog.twidda.model.Hashtag;
import org.nuclearfog.twidda.model.Location;
/**
* Hashtag implementation used by Mastodon API
@ -20,12 +20,15 @@ public class MastodonHashtag implements Hashtag {
private int popularity;
private String name;
private boolean following;
private long id;
private int rank;
/**
* @param json trend json object
*/
public MastodonHashtag(JSONObject json) {
JSONArray history = json.optJSONArray("history");
String idStr = json.optString("id", "0");
name = '#' + json.optString("name", "");
following = json.optBoolean("following", false);
if (history != null && history.length() > 0) {
@ -36,6 +39,17 @@ public class MastodonHashtag implements Hashtag {
} else {
popularity = json.optInt("statuses_count", 0);
}
try {
id = Long.parseLong(idStr);
} catch (NumberFormatException exception) {
// proceed without ID
}
}
@Override
public long getId() {
return id;
}
@ -53,7 +67,7 @@ public class MastodonHashtag implements Hashtag {
@Override
public int getRank() {
return -1;
return rank;
}
@ -68,6 +82,13 @@ public class MastodonHashtag implements Hashtag {
return following;
}
/**
*
*/
public void setRank(int rank) {
this.rank = rank;
}
@Override
public boolean equals(@Nullable Object obj) {

View File

@ -15,7 +15,7 @@ import org.nuclearfog.twidda.model.Hashtag;
*
* @author nuclearfog
*/
public class HashtagAction extends AsyncExecutor<HashtagAction.HashtagParam, HashtagAction.HashtagResult> {
public class HashtagAction extends AsyncExecutor<HashtagAction.HashtagActionParam, HashtagAction.HashtagActionResult> {
private Connection connection;
@ -28,41 +28,41 @@ public class HashtagAction extends AsyncExecutor<HashtagAction.HashtagParam, Has
@Override
protected HashtagResult doInBackground(@NonNull HashtagParam param) {
protected HashtagActionResult doInBackground(@NonNull HashtagActionParam param) {
try {
switch (param.mode) {
case HashtagParam.LOAD:
case HashtagActionParam.LOAD:
Hashtag result = connection.showHashtag(param.name);
return new HashtagResult(HashtagResult.LOAD, result, null);
return new HashtagActionResult(HashtagActionResult.LOAD, result, null);
case HashtagParam.FOLLOW:
case HashtagActionParam.FOLLOW:
result = connection.followHashtag(param.name);
return new HashtagResult(HashtagResult.FOLLOW, result, null);
return new HashtagActionResult(HashtagActionResult.FOLLOW, result, null);
case HashtagParam.UNFOLLOW:
case HashtagActionParam.UNFOLLOW:
result = connection.unfollowHashtag(param.name);
return new HashtagResult(HashtagResult.UNFOLLOW, result, null);
return new HashtagActionResult(HashtagActionResult.UNFOLLOW, result, null);
case HashtagParam.FEATURE:
case HashtagActionParam.FEATURE:
result = connection.featureHashtag(param.name);
return new HashtagResult(HashtagResult.FEATURE, result, null);
return new HashtagActionResult(HashtagActionResult.FEATURE, result, null);
case HashtagParam.UNFEATURE:
result = connection.unfeatureHashtag(param.name);
return new HashtagResult(HashtagResult.UNFEATURE, result, null);
case HashtagActionParam.UNFEATURE:
result = connection.unfeatureHashtag(param.id);
return new HashtagActionResult(HashtagActionResult.UNFEATURE, result, null);
default:
return null;
}
} catch (ConnectionException exception) {
return new HashtagResult(HashtagResult.ERROR, null, exception);
return new HashtagActionResult(HashtagActionResult.ERROR, null, exception);
}
}
/**
*
*/
public static class HashtagParam {
public static class HashtagActionParam {
public static final int LOAD = 1;
public static final int FOLLOW = 2;
@ -72,17 +72,25 @@ public class HashtagAction extends AsyncExecutor<HashtagAction.HashtagParam, Has
final String name;
final int mode;
final long id;
public HashtagParam(int mode, String name) {
public HashtagActionParam(int mode, String name) {
this.name = name;
this.mode = mode;
id = 0L;
}
public HashtagActionParam(int mode, String name, long id) {
this.name = name;
this.mode = mode;
this.id = id;
}
}
/**
*
*/
public static class HashtagResult {
public static class HashtagActionResult {
public static final int ERROR = -1;
public static final int LOAD = 10;
@ -97,7 +105,7 @@ public class HashtagAction extends AsyncExecutor<HashtagAction.HashtagParam, Has
public final Hashtag hashtag;
public final int mode;
HashtagResult(int mode, @Nullable Hashtag hashtag, @Nullable ConnectionException exception) {
HashtagActionResult(int mode, @Nullable Hashtag hashtag, @Nullable ConnectionException exception) {
this.exception = exception;
this.hashtag = hashtag;
this.mode = mode;

View File

@ -18,7 +18,7 @@ import org.nuclearfog.twidda.ui.fragments.HashtagFragment;
* @author nuclearfog
* @see HashtagFragment
*/
public class TrendLoader extends AsyncExecutor<TrendLoader.TrendParameter, TrendLoader.TrendResult> {
public class HashtagLoader extends AsyncExecutor<HashtagLoader.HashtagLoaderParam, HashtagLoader.HashtagLoaderResult> {
private Connection connection;
private AppDatabase db;
@ -26,52 +26,52 @@ public class TrendLoader extends AsyncExecutor<TrendLoader.TrendParameter, Trend
/**
*
*/
public TrendLoader(Context context) {
public HashtagLoader(Context context) {
connection = ConnectionManager.getDefaultConnection(context);
db = new AppDatabase(context);
}
@Override
protected TrendResult doInBackground(@NonNull TrendParameter param) {
protected HashtagLoaderResult doInBackground(@NonNull HashtagLoaderParam param) {
try {
switch (param.mode) {
case TrendParameter.POPULAR_OFFLINE:
case HashtagLoaderParam.POPULAR_OFFLINE:
Trends trends = db.getTrends();
if (!trends.isEmpty()) {
return new TrendResult(TrendResult.POPULAR, trends, param.index, null);
return new HashtagLoaderResult(HashtagLoaderResult.POPULAR, trends, param.index, null);
}
// fall through
case TrendParameter.POPULAR_ONLINE:
case HashtagLoaderParam.POPULAR_ONLINE:
trends = connection.getTrends();
db.saveTrends(trends);
return new TrendResult(TrendResult.POPULAR, trends, param.index, null);
return new HashtagLoaderResult(HashtagLoaderResult.POPULAR, trends, param.index, null);
case TrendParameter.SEARCH:
case HashtagLoaderParam.SEARCH:
trends = connection.searchHashtags(param.trend);
return new TrendResult(TrendResult.SEARCH, trends, param.index, null);
return new HashtagLoaderResult(HashtagLoaderResult.SEARCH, trends, param.index, null);
case TrendParameter.FOLLOWING:
case HashtagLoaderParam.FOLLOWING:
trends = connection.showHashtagFollowing(param.cursor);
return new TrendResult(TrendResult.FOLLOWING, trends, param.index, null);
return new HashtagLoaderResult(HashtagLoaderResult.FOLLOWING, trends, param.index, null);
case TrendParameter.FEATURING:
case HashtagLoaderParam.FEATURING:
trends = connection.showHashtagFeaturing();
return new TrendResult(TrendResult.FEATURING, trends, param.index, null);
return new HashtagLoaderResult(HashtagLoaderResult.FEATURING, trends, param.index, null);
default:
return null;
}
} catch (ConnectionException exception) {
return new TrendResult(TrendResult.ERROR, null, param.index, exception);
return new HashtagLoaderResult(HashtagLoaderResult.ERROR, null, param.index, exception);
}
}
/**
*
*/
public static class TrendParameter {
public static class HashtagLoaderParam {
public static final int POPULAR_OFFLINE = 1;
public static final int POPULAR_ONLINE = 2;
@ -86,7 +86,7 @@ public class TrendLoader extends AsyncExecutor<TrendLoader.TrendParameter, Trend
final int index;
final long cursor;
public TrendParameter(int mode, int index, String trend, long cursor) {
public HashtagLoaderParam(int mode, int index, String trend, long cursor) {
this.mode = mode;
this.trend = trend;
this.index = index;
@ -97,7 +97,7 @@ public class TrendLoader extends AsyncExecutor<TrendLoader.TrendParameter, Trend
/**
*
*/
public static class TrendResult {
public static class HashtagLoaderResult {
public static final int ERROR = -1;
public static final int POPULAR = 20;
@ -112,7 +112,7 @@ public class TrendLoader extends AsyncExecutor<TrendLoader.TrendParameter, Trend
@Nullable
public final ConnectionException exception;
TrendResult(int mode, @Nullable Trends trends, int index, @Nullable ConnectionException exception) {
HashtagLoaderResult(int mode, @Nullable Trends trends, int index, @Nullable ConnectionException exception) {
this.trends = trends;
this.exception = exception;
this.index = index;

View File

@ -304,7 +304,7 @@ public class AppDatabase {
/**
* select trends from trend table with given world ID
*/
private static final String TREND_SELECT = HashtagTable.ID + "=?";
private static final String TREND_SELECT = HashtagTable.LOCATION + "=?";
/**
* select status from status table matching ID
@ -537,10 +537,11 @@ public class AppDatabase {
db.delete(HashtagTable.NAME, TREND_SELECT, args);
for (Hashtag hashtag : hashtags) {
ContentValues column = new ContentValues(4);
column.put(HashtagTable.ID, hashtag.getLocationId());
column.put(HashtagTable.LOCATION, hashtag.getLocationId());
column.put(HashtagTable.VOL, hashtag.getPopularity());
column.put(HashtagTable.TREND, hashtag.getName());
column.put(HashtagTable.INDEX, hashtag.getRank());
column.put(HashtagTable.ID, hashtag.getId());
db.insert(HashtagTable.NAME, null, column);
}
adapter.commit();

View File

@ -20,7 +20,7 @@ public class DatabaseAdapter {
/**
* database version
*/
private static final int DB_VERSION = 20;
private static final int DB_VERSION = 21;
/**
* database file name
@ -78,6 +78,7 @@ public class DatabaseAdapter {
private static final String TABLE_TRENDS = "CREATE TABLE IF NOT EXISTS "
+ HashtagTable.NAME + "("
+ HashtagTable.ID + " INTEGER,"
+ HashtagTable.LOCATION + " INTEGER,"
+ HashtagTable.INDEX + " INTEGER,"
+ HashtagTable.VOL + " INTEGER,"
+ HashtagTable.TREND + " TEXT);";
@ -306,6 +307,11 @@ public class DatabaseAdapter {
*/
private static final String UPDATE_MEDIA_ADD_BLUR_HASH = "ALTER TABLE " + MediaTable.NAME + " ADD " + MediaTable.BLUR + " TEXT;";
/**
* add mediatable description
*/
private static final String UPDATE_HASHTAG_ADD_ID = "ALTER TABLE " + HashtagTable.NAME + " ADD " + HashtagTable.ID + " INTEGER;";
/**
* singleton instance
*/
@ -407,9 +413,13 @@ public class DatabaseAdapter {
db.execSQL(UPDATE_MEDIA_ADD_BLUR_HASH);
db.setVersion(19);
}
if (db.getVersion() < DB_VERSION) {
if (db.getVersion() < 20) {
db.delete(EmojiTable.NAME, null, null);
db.execSQL(TABLE_EMOJI);
db.setVersion(20);
}
if (db.getVersion() < DB_VERSION) {
db.execSQL(UPDATE_HASHTAG_ADD_ID);
db.setVersion(DB_VERSION);
}
}
@ -701,10 +711,15 @@ public class DatabaseAdapter {
*/
String NAME = "trend";
/**
* id of the hashtag
*/
String ID = "id";
/**
* Location ID
*/
String ID = "woeID";
String LOCATION = "woeID";
/**
* rank of the hashtag

View File

@ -20,12 +20,13 @@ public class DatabaseHashtag implements Hashtag, HashtagTable {
/**
* SQLite columns
*/
public static final String[] COLUMNS = {TREND, VOL, INDEX, ID};
public static final String[] COLUMNS = {TREND, VOL, INDEX, LOCATION, ID};
private String name = "";
private int popularity;
private int rank;
private long id;
private long locationId;
/**
* @param cursor database cursor using this {@link #COLUMNS} projection
@ -34,13 +35,20 @@ public class DatabaseHashtag implements Hashtag, HashtagTable {
String name = cursor.getString(0);
popularity = cursor.getInt(1);
rank = cursor.getInt(2);
id = cursor.getLong(3);
locationId = cursor.getLong(3);
id = cursor.getLong(4);
if (name != null) {
this.name = name;
}
}
@Override
public long getId() {
return id;
}
@Override
public String getName() {
return name;
@ -49,7 +57,7 @@ public class DatabaseHashtag implements Hashtag, HashtagTable {
@Override
public long getLocationId() {
return id;
return locationId;
}
@ -83,6 +91,6 @@ public class DatabaseHashtag implements Hashtag, HashtagTable {
@NonNull
@Override
public String toString() {
return "name=\"" + getName() + "\"";
return "name=\"" + getName() + "\" rank=" + rank;
}
}

View File

@ -10,27 +10,32 @@ import java.io.Serializable;
public interface Hashtag extends Serializable, Comparable<Hashtag> {
/**
* @return trend name
* @return hashtag name
*/
String getName();
/**
* @return ID of the trend location
* @return trend ID if any
*/
long getId();
/**
* @return hashtag ID
*/
long getLocationId();
/**
* @return rank of the trend
* @return rank of the hashtag if any
*/
int getRank();
/**
* @return popularity of the trend
* @return popularity of the hashtag
*/
int getPopularity();
/**
* @return true if current user follows trend (hashtag)
* @return true if current user follows hashtag
*/
boolean following();

View File

@ -17,8 +17,8 @@ import androidx.viewpager2.widget.ViewPager2;
import org.nuclearfog.twidda.R;
import org.nuclearfog.twidda.backend.async.AsyncExecutor.AsyncCallback;
import org.nuclearfog.twidda.backend.async.HashtagAction;
import org.nuclearfog.twidda.backend.async.HashtagAction.HashtagParam;
import org.nuclearfog.twidda.backend.async.HashtagAction.HashtagResult;
import org.nuclearfog.twidda.backend.async.HashtagAction.HashtagActionParam;
import org.nuclearfog.twidda.backend.async.HashtagAction.HashtagActionResult;
import org.nuclearfog.twidda.backend.utils.AppStyles;
import org.nuclearfog.twidda.config.GlobalSettings;
import org.nuclearfog.twidda.ui.adapter.viewpager.HashtagAdapter;
@ -30,7 +30,7 @@ import org.nuclearfog.twidda.ui.views.TabSelector.OnTabSelectedListener;
*
* @author nuclearfog
*/
public class HashtagActivity extends AppCompatActivity implements SearchView.OnQueryTextListener, OnTabSelectedListener, AsyncCallback<HashtagResult> {
public class HashtagActivity extends AppCompatActivity implements SearchView.OnQueryTextListener, OnTabSelectedListener, AsyncCallback<HashtagActionResult> {
private GlobalSettings settings;
private HashtagAction hashtagAction;
@ -90,11 +90,11 @@ public class HashtagActivity extends AppCompatActivity implements SearchView.OnQ
public boolean onQueryTextSubmit(String query) {
if (hashtagAction.isIdle()) {
if (viewPager.getCurrentItem() == 0) {
HashtagParam param = new HashtagParam(HashtagParam.FOLLOW, query);
HashtagActionParam param = new HashtagActionParam(HashtagActionParam.FOLLOW, query);
hashtagAction.execute(param, this);
return true;
} else if (viewPager.getCurrentItem() == 1) {
HashtagParam param = new HashtagParam(HashtagParam.FEATURE, query);
HashtagActionParam param = new HashtagActionParam(HashtagActionParam.FEATURE, query);
hashtagAction.execute(param, this);
return true;
}
@ -110,21 +110,21 @@ public class HashtagActivity extends AppCompatActivity implements SearchView.OnQ
@Override
public void onResult(@NonNull HashtagResult result) {
public void onResult(@NonNull HashtagActionResult result) {
switch (result.mode) {
case HashtagResult.FEATURE:
case HashtagActionResult.FEATURE:
Toast.makeText(getApplicationContext(), R.string.info_hashtag_featured, Toast.LENGTH_SHORT).show();
adapter.notifySettingsChanged();
invalidateOptionsMenu();
break;
case HashtagResult.FOLLOW:
case HashtagActionResult.FOLLOW:
Toast.makeText(getApplicationContext(), R.string.info_hashtag_followed, Toast.LENGTH_SHORT).show();
adapter.notifySettingsChanged();
invalidateOptionsMenu();
break;
case HashtagResult.ERROR:
case HashtagActionResult.ERROR:
break;
}
}

View File

@ -22,8 +22,8 @@ import androidx.viewpager2.widget.ViewPager2;
import org.nuclearfog.twidda.R;
import org.nuclearfog.twidda.backend.async.AsyncExecutor.AsyncCallback;
import org.nuclearfog.twidda.backend.async.HashtagAction;
import org.nuclearfog.twidda.backend.async.HashtagAction.HashtagParam;
import org.nuclearfog.twidda.backend.async.HashtagAction.HashtagResult;
import org.nuclearfog.twidda.backend.async.HashtagAction.HashtagActionParam;
import org.nuclearfog.twidda.backend.async.HashtagAction.HashtagActionResult;
import org.nuclearfog.twidda.backend.utils.AppStyles;
import org.nuclearfog.twidda.backend.utils.ErrorUtils;
import org.nuclearfog.twidda.config.GlobalSettings;
@ -39,7 +39,7 @@ import java.io.Serializable;
*
* @author nuclearfog
*/
public class SearchActivity extends AppCompatActivity implements OnClickListener, OnTabSelectedListener, OnQueryTextListener, AsyncCallback<HashtagResult> {
public class SearchActivity extends AppCompatActivity implements OnClickListener, OnTabSelectedListener, OnQueryTextListener, AsyncCallback<HashtagActionResult> {
/**
* Key for the search query, required
@ -96,7 +96,7 @@ public class SearchActivity extends AppCompatActivity implements OnClickListener
} else if (query != null) {
search = query;
if (search.startsWith("#") && search.matches("\\S+")) {
HashtagParam param = new HashtagParam(HashtagParam.LOAD, search);
HashtagActionParam param = new HashtagActionParam(HashtagActionParam.LOAD, search);
hashtagAction.execute(param, this);
}
}
@ -207,11 +207,11 @@ public class SearchActivity extends AppCompatActivity implements OnClickListener
// follow/unfollow hashtag
else if (item.getItemId() == R.id.search_hashtag) {
if (hashtag != null && hashtagAction.isIdle()) {
HashtagParam param;
HashtagActionParam param;
if (hashtag.following())
param = new HashtagParam(HashtagParam.UNFOLLOW, hashtag.getName());
param = new HashtagActionParam(HashtagActionParam.UNFOLLOW, hashtag.getName());
else
param = new HashtagParam(HashtagParam.FOLLOW, hashtag.getName());
param = new HashtagActionParam(HashtagActionParam.FOLLOW, hashtag.getName());
hashtagAction.execute(param, this);
}
}
@ -258,21 +258,21 @@ public class SearchActivity extends AppCompatActivity implements OnClickListener
@Override
public void onResult(@NonNull HashtagResult result) {
public void onResult(@NonNull HashtagActionResult result) {
if (result.hashtag != null) {
this.hashtag = result.hashtag;
invalidateMenu();
}
switch (result.mode) {
case HashtagResult.FOLLOW:
case HashtagActionResult.FOLLOW:
Toast.makeText(getApplicationContext(), R.string.info_hashtag_followed, Toast.LENGTH_SHORT).show();
break;
case HashtagResult.UNFOLLOW:
case HashtagActionResult.UNFOLLOW:
Toast.makeText(getApplicationContext(), R.string.info_hashtag_unfollowed, Toast.LENGTH_SHORT).show();
break;
case HashtagResult.ERROR:
case HashtagActionResult.ERROR:
ErrorUtils.showErrorMessage(this, result.exception);
break;
}

View File

@ -10,7 +10,7 @@ import org.nuclearfog.twidda.model.Hashtag;
import org.nuclearfog.twidda.model.lists.Trends;
import org.nuclearfog.twidda.ui.adapter.recyclerview.holder.OnHolderClickListener;
import org.nuclearfog.twidda.ui.adapter.recyclerview.holder.PlaceHolder;
import org.nuclearfog.twidda.ui.adapter.recyclerview.holder.TrendHolder;
import org.nuclearfog.twidda.ui.adapter.recyclerview.holder.HashtagHolder;
/**
* custom {@link androidx.recyclerview.widget.RecyclerView} adapter implementation to show trends
@ -18,7 +18,7 @@ import org.nuclearfog.twidda.ui.adapter.recyclerview.holder.TrendHolder;
* @author nuclearfog
* @see org.nuclearfog.twidda.ui.fragments.HashtagFragment
*/
public class TrendAdapter extends Adapter<ViewHolder> implements OnHolderClickListener {
public class HashtagAdapter extends Adapter<ViewHolder> implements OnHolderClickListener {
/**
* "index" used to replace the whole list with new items
@ -31,15 +31,16 @@ public class TrendAdapter extends Adapter<ViewHolder> implements OnHolderClickLi
private static final int NO_LOADING = -1;
private TrendClickListener itemClickListener;
private OnHashtagClickListener itemClickListener;
private Trends items = new Trends();
private int loadingIndex = NO_LOADING;
private boolean enableDelete = false;
/**
* @param itemClickListener Listener for item click
*/
public TrendAdapter(TrendClickListener itemClickListener) {
public HashtagAdapter(OnHashtagClickListener itemClickListener) {
this.itemClickListener = itemClickListener;
}
@ -62,7 +63,7 @@ public class TrendAdapter extends Adapter<ViewHolder> implements OnHolderClickLi
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (viewType == TYPE_TREND) {
return new TrendHolder(parent, this);
return new HashtagHolder(parent, this, enableDelete);
} else {
return new PlaceHolder(parent, this, false);
}
@ -71,8 +72,8 @@ public class TrendAdapter extends Adapter<ViewHolder> implements OnHolderClickLi
@Override
public void onBindViewHolder(@NonNull ViewHolder vh, int index) {
if (vh instanceof TrendHolder) {
TrendHolder holder = (TrendHolder) vh;
if (vh instanceof HashtagHolder) {
HashtagHolder holder = (HashtagHolder) vh;
Hashtag hashtag = items.get(index);
if (hashtag != null) {
holder.setContent(hashtag, index);
@ -86,7 +87,11 @@ public class TrendAdapter extends Adapter<ViewHolder> implements OnHolderClickLi
@Override
public void onItemClick(int position, int type, int... extras) {
itemClickListener.onTrendClick(items.get(position));
if (type == HASHTAG_CLICK) {
itemClickListener.onHashtagClick(items.get(position), OnHashtagClickListener.SELECT);
} else if (type == HASHTAG_REMOVE) {
itemClickListener.onHashtagClick(items.get(position), OnHashtagClickListener.REMOVE);
}
}
@ -177,18 +182,32 @@ public class TrendAdapter extends Adapter<ViewHolder> implements OnHolderClickLi
}
/**
* Listener for trend list
*
*/
public interface TrendClickListener {
public void enableDelete() {
enableDelete = true;
}
/**
* Listener for hashtag list
*/
public interface OnHashtagClickListener {
int SELECT = 1;
int REMOVE = 2;
/**
* called when a trend item is clicked
*
* @param hashtag trend name
* @param action action to take {@link #SELECT,#REMOVE}
*/
void onTrendClick(Hashtag hashtag);
void onHashtagClick(Hashtag hashtag, int action);
/**
*
*/
boolean onPlaceholderClick(long cursor, int index);
}
}

View File

@ -17,26 +17,28 @@ import org.nuclearfog.twidda.backend.utils.AppStyles;
import org.nuclearfog.twidda.backend.utils.StringUtils;
import org.nuclearfog.twidda.config.GlobalSettings;
import org.nuclearfog.twidda.model.Hashtag;
import org.nuclearfog.twidda.ui.adapter.recyclerview.HashtagAdapter;
/**
* ViewHolder for a trend item
*
* @author nuclearfog
* @see org.nuclearfog.twidda.ui.adapter.recyclerview.TrendAdapter
* @see HashtagAdapter
*/
public class TrendHolder extends ViewHolder implements OnClickListener {
public class HashtagHolder extends ViewHolder implements OnClickListener {
private TextView name, rank, vol;
private OnHolderClickListener listener;
public TrendHolder(ViewGroup parent, OnHolderClickListener listener) {
super(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_trend, parent, false));
public HashtagHolder(ViewGroup parent, OnHolderClickListener listener, boolean enableRemove) {
super(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_hashtag, parent, false));
this.listener = listener;
CardView background = (CardView) itemView;
ViewGroup container = itemView.findViewById(R.id.item_trend_container);
View btnRemove = itemView.findViewById(R.id.item_trend_delete_button);
rank = itemView.findViewById(R.id.item_trend_rank);
name = itemView.findViewById(R.id.item_trend_name);
vol = itemView.findViewById(R.id.item_trend_vol);
@ -44,16 +46,26 @@ public class TrendHolder extends ViewHolder implements OnClickListener {
GlobalSettings settings = GlobalSettings.get(parent.getContext());
AppStyles.setTheme(container, Color.TRANSPARENT);
background.setCardBackgroundColor(settings.getCardColor());
if (enableRemove) {
btnRemove.setVisibility(View.VISIBLE);
} else {
btnRemove.setVisibility(View.GONE);
}
itemView.setOnClickListener(this);
btnRemove.setOnClickListener(this);
}
@Override
public void onClick(View v) {
if (v == itemView) {
int position = getLayoutPosition();
if (v.getId() == R.id.item_trend_delete_button) {
if (position != RecyclerView.NO_POSITION) {
listener.onItemClick(position, OnHolderClickListener.NO_TYPE);
listener.onItemClick(position, OnHolderClickListener.HASHTAG_REMOVE);
}
} else if (v == itemView) {
if (position != RecyclerView.NO_POSITION) {
listener.onItemClick(position, OnHolderClickListener.HASHTAG_CLICK);
}
}
}

View File

@ -45,6 +45,10 @@ public interface OnHolderClickListener {
int FILTER_REMOVE = 23;
int HASHTAG_CLICK = 24;
int HASHTAG_REMOVE = 25;
/**
* called when an item was clicked
*

View File

@ -84,7 +84,7 @@ public class UserHolder extends ViewHolder implements OnClickListener, AsyncCall
profileImg = itemView.findViewById(R.id.item_user_profile);
verifyIcon = itemView.findViewById(R.id.item_user_verified);
lockedIcon = itemView.findViewById(R.id.item_user_private);
delete = itemView.findViewById(R.id.item_user_delete_buton);
delete = itemView.findViewById(R.id.item_user_delete_button);
placeholder = new ColorDrawable(EMPTY_COLOR);
AppStyles.setTheme(container, Color.TRANSPARENT);

View File

@ -134,6 +134,16 @@ public class ConfirmDialog extends Dialog implements OnClickListener {
*/
public static final int CONTINUE_BROWSER = 627;
/**
*
*/
public static final int UNFOLLOW_HASHTAG = 628;
/**
*
*/
public static final int UNFEATURE_HASHTAG = 629;
private TextView title, message, remember_label;
private Button confirm, cancel;
@ -275,6 +285,14 @@ public class ConfirmDialog extends Dialog implements OnClickListener {
titleRes = R.string.confirm_warning;
messageRes = R.string.confirm_proxy_bypass;
break;
case UNFOLLOW_HASHTAG:
messageRes = R.string.confirm_hashtag_unfollow;
break;
case UNFEATURE_HASHTAG:
messageRes = R.string.confirm_hashtag_unfeature;
break;
}
// setup title
title.setVisibility(titleVis);

View File

@ -3,6 +3,7 @@ package org.nuclearfog.twidda.ui.fragments;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultCallback;
@ -11,16 +12,21 @@ 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.TrendLoader;
import org.nuclearfog.twidda.backend.async.TrendLoader.TrendParameter;
import org.nuclearfog.twidda.backend.async.TrendLoader.TrendResult;
import org.nuclearfog.twidda.backend.async.HashtagAction;
import org.nuclearfog.twidda.backend.async.HashtagAction.HashtagActionParam;
import org.nuclearfog.twidda.backend.async.HashtagAction.HashtagActionResult;
import org.nuclearfog.twidda.backend.async.HashtagLoader;
import org.nuclearfog.twidda.backend.async.HashtagLoader.HashtagLoaderParam;
import org.nuclearfog.twidda.backend.async.HashtagLoader.HashtagLoaderResult;
import org.nuclearfog.twidda.backend.utils.ErrorUtils;
import org.nuclearfog.twidda.model.Hashtag;
import org.nuclearfog.twidda.model.lists.Trends;
import org.nuclearfog.twidda.ui.activities.SearchActivity;
import org.nuclearfog.twidda.ui.adapter.recyclerview.TrendAdapter;
import org.nuclearfog.twidda.ui.adapter.recyclerview.TrendAdapter.TrendClickListener;
import org.nuclearfog.twidda.ui.adapter.recyclerview.HashtagAdapter;
import org.nuclearfog.twidda.ui.adapter.recyclerview.HashtagAdapter.OnHashtagClickListener;
import org.nuclearfog.twidda.ui.dialogs.ConfirmDialog;
import java.io.Serializable;
@ -29,7 +35,7 @@ import java.io.Serializable;
*
* @author nuclearfog
*/
public class HashtagFragment extends ListFragment implements TrendClickListener, AsyncCallback<TrendResult>, ActivityResultCallback<ActivityResult> {
public class HashtagFragment extends ListFragment implements OnHashtagClickListener, ActivityResultCallback<ActivityResult>, ConfirmDialog.OnConfirmListener {
/**
* setup fragment to show popular trends of an instance/location
@ -72,18 +78,26 @@ public class HashtagFragment extends ListFragment implements TrendClickListener,
private ActivityResultLauncher<Intent> activityResultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), this);
private TrendLoader trendLoader;
private TrendAdapter adapter;
private AsyncCallback<HashtagActionResult> hashtagActionCallback = this::onHashtagActionResult;
private AsyncCallback<HashtagLoaderResult> hashtagLoaderCallback = this::onHashtagLoaderResult;
private HashtagLoader hashtagLoader;
private HashtagAction hashtagAction;
private HashtagAdapter adapter;
private ConfirmDialog confirmDialog;
private int mode = 0;
private String search = "";
private Hashtag selection;
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
adapter = new TrendAdapter(this);
trendLoader = new TrendLoader(requireContext());
adapter = new HashtagAdapter(this);
hashtagLoader = new HashtagLoader(requireContext());
hashtagAction = new HashtagAction(requireContext());
confirmDialog = new ConfirmDialog(requireActivity(), this);
setAdapter(adapter);
Bundle args = getArguments();
@ -91,15 +105,18 @@ public class HashtagFragment extends ListFragment implements TrendClickListener,
search = args.getString(KEY_SEARCH, "");
mode = args.getInt(KEY_MODE, 0);
}
if (mode == MODE_FOLLOW || mode == MODE_FEATURE) {
adapter.enableDelete();
}
if (savedInstanceState != null) {
Serializable data = savedInstanceState.getSerializable(KEY_DATA);
if (data instanceof Trends) {
adapter.addItems((Trends) data, TrendAdapter.CLEAR_LIST);
adapter.addItems((Trends) data, HashtagAdapter.CLEAR_LIST);
return;
}
}
setRefresh(true);
load(TrendParameter.NO_CURSOR, TrendAdapter.CLEAR_LIST);
load(HashtagLoaderParam.NO_CURSOR, HashtagAdapter.CLEAR_LIST);
}
@ -112,7 +129,7 @@ public class HashtagFragment extends ListFragment implements TrendClickListener,
@Override
public void onDestroy() {
trendLoader.cancel();
hashtagLoader.cancel();
super.onDestroy();
}
@ -120,15 +137,15 @@ public class HashtagFragment extends ListFragment implements TrendClickListener,
@Override
protected void onReset() {
adapter.clear();
trendLoader = new TrendLoader(requireContext());
load(TrendParameter.NO_CURSOR, TrendAdapter.CLEAR_LIST);
hashtagLoader = new HashtagLoader(requireContext());
load(HashtagLoaderParam.NO_CURSOR, HashtagAdapter.CLEAR_LIST);
setRefresh(true);
}
@Override
protected void onReload() {
load(TrendParameter.NO_CURSOR, TrendAdapter.CLEAR_LIST);
load(HashtagLoaderParam.NO_CURSOR, HashtagAdapter.CLEAR_LIST);
}
@ -150,7 +167,8 @@ public class HashtagFragment extends ListFragment implements TrendClickListener,
@Override
public void onTrendClick(Hashtag hashtag) {
public void onHashtagClick(Hashtag hashtag, int action) {
if (action == OnHashtagClickListener.SELECT) {
if (!isRefreshing()) {
Intent intent = new Intent(requireContext(), SearchActivity.class);
String name = hashtag.getName();
@ -162,12 +180,22 @@ public class HashtagFragment extends ListFragment implements TrendClickListener,
}
activityResultLauncher.launch(intent);
}
} else if (action == OnHashtagClickListener.REMOVE) {
if (!confirmDialog.isShowing()) {
selection = hashtag;
if (mode == MODE_FEATURE) {
confirmDialog.show(ConfirmDialog.UNFEATURE_HASHTAG);
} else if (mode == MODE_FOLLOW) {
confirmDialog.show(ConfirmDialog.UNFOLLOW_HASHTAG);
}
}
}
}
@Override
public boolean onPlaceholderClick(long cursor, int index) {
if (trendLoader.isIdle()) {
if (hashtagLoader.isIdle()) {
load(cursor, index);
return true;
}
@ -176,8 +204,36 @@ public class HashtagFragment extends ListFragment implements TrendClickListener,
@Override
public void onResult(@NonNull TrendResult result) {
if (result.mode == TrendResult.ERROR) {
public void onConfirm(int type, boolean remember) {
if (selection != null) {
if (type == ConfirmDialog.UNFOLLOW_HASHTAG) {
HashtagActionParam param = new HashtagActionParam(HashtagActionParam.UNFOLLOW, selection.getName(), selection.getId());
hashtagAction.execute(param, hashtagActionCallback);
} else if (type == ConfirmDialog.UNFEATURE_HASHTAG) {
HashtagActionParam param = new HashtagActionParam(HashtagActionParam.UNFEATURE, selection.getName(), selection.getId());
hashtagAction.execute(param, hashtagActionCallback);
}
}
}
/**
* callback for {@link HashtagAction}
*/
private void onHashtagActionResult(@NonNull HashtagActionResult result) {
if (result.mode == HashtagActionResult.UNFEATURE) {
Toast.makeText(requireContext(), R.string.info_hashtag_unfeatured, Toast.LENGTH_SHORT).show();
adapter.removeItem(result.hashtag);
} else if (result.mode == HashtagActionResult.UNFOLLOW) {
Toast.makeText(requireContext(), R.string.info_hashtag_unfollowed, Toast.LENGTH_SHORT).show();
adapter.removeItem(result.hashtag);
}
}
/**
* callback for {@link HashtagLoader}
*/
private void onHashtagLoaderResult(@NonNull HashtagLoaderResult result) {
if (result.mode == HashtagLoaderResult.ERROR) {
if (getContext() != null) {
ErrorUtils.showErrorMessage(getContext(), result.exception);
}
@ -192,30 +248,30 @@ public class HashtagFragment extends ListFragment implements TrendClickListener,
* load content into the list
*/
private void load(long cursor, int index) {
TrendParameter param;
HashtagLoaderParam param;
switch (mode) {
case MODE_POPULAR:
if (adapter.isEmpty()) {
param = new TrendParameter(TrendParameter.POPULAR_OFFLINE, index, search, cursor);
param = new HashtagLoaderParam(HashtagLoaderParam.POPULAR_OFFLINE, index, search, cursor);
} else {
param = new TrendParameter(TrendParameter.POPULAR_ONLINE, index, search, cursor);
param = new HashtagLoaderParam(HashtagLoaderParam.POPULAR_ONLINE, index, search, cursor);
}
trendLoader.execute(param, this);
hashtagLoader.execute(param, hashtagLoaderCallback);
break;
case MODE_FOLLOW:
param = new TrendParameter(TrendParameter.FOLLOWING, index, search, cursor);
trendLoader.execute(param, this);
param = new HashtagLoaderParam(HashtagLoaderParam.FOLLOWING, index, search, cursor);
hashtagLoader.execute(param, hashtagLoaderCallback);
break;
case MODE_FEATURE:
param = new TrendParameter(TrendParameter.FEATURING, index, search, cursor);
trendLoader.execute(param, this);
param = new HashtagLoaderParam(HashtagLoaderParam.FEATURING, index, search, cursor);
hashtagLoader.execute(param, hashtagLoaderCallback);
break;
case MODE_SEARCH:
param = new TrendParameter(TrendParameter.SEARCH, index, search, cursor);
trendLoader.execute(param, this);
param = new HashtagLoaderParam(HashtagLoaderParam.SEARCH, index, search, cursor);
hashtagLoader.execute(param, hashtagLoaderCallback);
break;
}
}

View File

@ -9,16 +9,16 @@
android:id="@+id/item_trend_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/trenditem_layout_padding">
android:padding="@dimen/item_hashtag_layout_padding">
<TextView
android:id="@+id/item_trend_rank"
android:layout_width="@dimen/trenditem_textsize_trendindex_width"
android:layout_width="@dimen/item_hashtag_textsize_trendindex_width"
android:layout_height="wrap_content"
android:gravity="center_vertical|end"
android:lines="1"
android:textAlignment="gravity"
android:textSize="@dimen/trenditem_textsize_trendindex"
android:textSize="@dimen/item_hashtag_textsize_trendindex"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@id/item_trend_name"
@ -28,28 +28,42 @@
android:id="@+id/item_trend_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/trenditem_text_margin"
android:layout_marginStart="@dimen/item_hashtag_layout_margin"
android:layout_marginEnd="@dimen/item_hashtag_layout_margin"
android:lines="1"
android:textSize="@dimen/trenditem_textsize_trendname"
android:textSize="@dimen/item_hashtag_textsize_trendname"
app:layout_constraintStart_toEndOf="@id/item_trend_rank"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/item_trend_vol"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toStartOf="@id/item_trend_delete_button"
app:layout_constrainedWidth="true" />
<TextView
android:id="@+id/item_trend_vol"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/trenditem_text_margin"
android:layout_marginStart="@dimen/item_hashtag_layout_margin"
android:lines="1"
android:textSize="@dimen/trenditem_textsize_trendvol"
android:textSize="@dimen/item_hashtag_textsize_trendvol"
app:layout_constraintStart_toEndOf="@id/item_trend_rank"
app:layout_constraintTop_toBottomOf="@id/item_trend_name"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constrainedWidth="false" />
<ImageButton
android:id="@+id/item_trend_delete_button"
android:layout_width="@dimen/item_hashtag_button_size"
android:layout_height="@dimen/item_hashtag_button_size"
android:visibility="invisible"
android:padding="@dimen/item_hashtag_button_padding"
android:contentDescription="@string/descr_remove_hashtag"
android:scaleType="fitCenter"
android:src="@drawable/cross"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
style="@style/RoundButton" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

View File

@ -80,7 +80,7 @@
app:layout_constraintStart_toEndOf="@id/item_user_verified"
app:layout_constraintTop_toTopOf="@id/item_user_profile"
app:layout_constraintBottom_toTopOf="@id/item_user_screenname"
app:layout_constraintEnd_toStartOf="@id/item_user_delete_buton" />
app:layout_constraintEnd_toStartOf="@id/item_user_delete_button" />
<ImageView
android:id="@+id/item_user_private"
@ -106,7 +106,7 @@
app:layout_constraintStart_toEndOf="@id/item_user_private"
app:layout_constraintTop_toBottomOf="@id/item_user_username"
app:layout_constraintBottom_toTopOf="@id/item_user_following_icon"
app:layout_constraintEnd_toStartOf="@id/item_user_delete_buton" />
app:layout_constraintEnd_toStartOf="@id/item_user_delete_button" />
<ImageView
android:id="@+id/item_user_following_icon"
@ -157,10 +157,10 @@
app:layout_constraintStart_toEndOf="@id/item_user_follower_icon"
app:layout_constraintTop_toTopOf="@id/item_user_follower_icon"
app:layout_constraintBottom_toBottomOf="@id/item_user_follower_icon"
app:layout_constraintEnd_toStartOf="@id/item_user_delete_buton" />
app:layout_constraintEnd_toStartOf="@id/item_user_delete_button" />
<ImageButton
android:id="@+id/item_user_delete_buton"
android:id="@+id/item_user_delete_button"
android:layout_width="@dimen/item_user_button_size"
android:layout_height="@dimen/item_user_button_size"
android:visibility="invisible"

View File

@ -209,6 +209,7 @@
<string name="menu_hint_mute_user">\@name stummschalten</string>
<string name="menu_hint_block_user">\@name blockieren</string>
<string name="menu_exclude_user">Nutzer ausschließen</string>
<string name="menu_hashtag_add">Hashtag hinzufügen</string>
<string name="menu_search_filter">Ergebnisse filtern</string>
<string name="menu_hint_block_domain">domain.namen eingeben</string>
<string name="menu_licenses">Lizenzen</string>
@ -296,11 +297,13 @@
<string name="error_duration_time_low">Dauer der Umfrage ist zu kurz!</string>
<string name="error_duration_time_high">Dauer der Umfrage ist zu lang!</string>
<string name="error_poll_option_missing">Umfrageoption darf nicht leer sein!</string>
<string name="menu_hashtags">Hashtags</string>
<string name="menu_hashtag_follow">folge Hashtag</string>
<string name="menu_hashtag_unfollow">entfolge Hashtag</string>
<string name="info_hashtag_unfollowed">Hashtag entfolgt</string>
<string name="info_domain_removed">Domain von der Liste entfernt</string>
<string name="info_hashtag_followed">Hashtag gefolgt</string>
<string name="dialog_status_schedule_set">Veröffentlichung planen</string>
<string name="dialog_push_repost">bei Reposts</string>
<string name="dialog_push_favorite">bei Favorisierung</string>
<string name="dialog_push_poll_finished">bei beendeten Umfragen</string>
@ -354,4 +357,6 @@
<string name="error_empty_filter_title">Filterbezeichnung darf nicht leer sein</string>
<string name="error_empty_filter_selection">es muss mindestens ein Filter aktiviert sein</string>
<string name="dialog_description_title">Medienbeschreibung</string>
<string name="descr_remove_hashtag">Hashtag von der Liste entfernen</string>
<string name="confirm_hashtag_unfollow">Hashtag entfolgen?</string>
</resources>

View File

@ -92,13 +92,15 @@
<dimen name="item_status_indicator_padding">6sp</dimen>
<dimen name="item_status_indicator_size">26sp</dimen>
<!--dimens of item_trend.xml-->
<dimen name="trenditem_layout_padding">5dp</dimen>
<dimen name="trenditem_text_margin">5dp</dimen>
<dimen name="trenditem_textsize_trendindex">20sp</dimen>
<dimen name="trenditem_textsize_trendname">20sp</dimen>
<dimen name="trenditem_textsize_trendvol">14sp</dimen>
<dimen name="trenditem_textsize_trendindex_width">40sp</dimen>
<!--dimens of item_hashtag.xml-->
<dimen name="item_hashtag_layout_padding">5dp</dimen>
<dimen name="item_hashtag_layout_margin">5dp</dimen>
<dimen name="item_hashtag_button_size">20sp</dimen>
<dimen name="item_hashtag_button_padding">1dp</dimen>
<dimen name="item_hashtag_textsize_trendindex">20sp</dimen>
<dimen name="item_hashtag_textsize_trendname">20sp</dimen>
<dimen name="item_hashtag_textsize_trendvol">14sp</dimen>
<dimen name="item_hashtag_textsize_trendindex_width">40sp</dimen>
<!--dimens of item_user.xml-->
<dimen name="item_user_image_size">56sp</dimen>

View File

@ -78,6 +78,7 @@
<string name="info_hashtag_featured">Hashtag featured</string>
<string name="info_hashtag_followed">Hashtag followed</string>
<string name="info_hashtag_unfollowed">Hashtag unfollowed</string>
<string name="info_hashtag_unfeatured">Hashtag unfeatured</string>
<string name="info_status_reported">Status reported</string>
<string name="info_error">Error</string>
@ -289,6 +290,7 @@
<string name="update_list">update list</string>
<string name="confirm_remove_user_from_list">remove user from list?</string>
<string name="descr_remove_user">remove user from list</string>
<string name="descr_remove_hashtag">remove hashtag from list</string>
<string name="descr_remove_domain">remove domain from list</string>
<string name="confirm_unknown_error">unknown error!</string>
<string name="list_following_indicator">following list</string>
@ -307,6 +309,8 @@
<string name="confirm_remember">remember choice</string>
<string name="confirm_warning">Warning</string>
<string name="confirm_proxy_bypass">Opening an external link would bypass proxy connection. Proceed?</string>
<string name="confirm_hashtag_unfollow">unfollow hashtag?</string>
<string name="confirm_hashtag_unfeature">unfeature hashtag?</string>
<string name="confirm_remove_account">remove account from list?</string>
<string name="confirm_remove_filter">delete filter?</string>
<string name="account_user_unnamed">\'unnamed\'</string>