Compare commits
29 Commits
v1.0.3
...
v1.0.4-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12599db0ff | ||
|
|
c751c85c1c | ||
|
|
f1331a0f6d | ||
|
|
c75c9b60f9 | ||
|
|
eb3adf1dfd | ||
|
|
6533163fd0 | ||
|
|
fa75570254 | ||
|
|
1406ea376d | ||
|
|
1becad6016 | ||
|
|
d34653750e | ||
|
|
705592aefd | ||
|
|
583325d6e8 | ||
|
|
318d271127 | ||
|
|
5562bf936e | ||
|
|
02a1f2ef8c | ||
|
|
7b26649521 | ||
|
|
8059120136 | ||
|
|
ec38210dde | ||
|
|
38eadca4e2 | ||
|
|
31cb17d549 | ||
|
|
10a5bf0a82 | ||
|
|
a58a279e8c | ||
|
|
0fe58e49b6 | ||
|
|
089e297656 | ||
|
|
2a65bdb08f | ||
|
|
93fbc52f6a | ||
|
|
4e4b5fcfe4 | ||
|
|
620bc2285c | ||
|
|
f73849dbb7 |
14
README.md
14
README.md
@@ -1,11 +1,17 @@
|
||||
# Mastodon for Android
|
||||
# Forked Mastodon for Android
|
||||
[](https://crowdin.com/project/mastodon-for-android)
|
||||
|
||||
<a href="https://play.google.com/store/apps/details?id=org.joinmastodon.android"><img src="img/google-play-badge.png" height="50"></a>
|
||||
|
||||
This is the repository for the official Android app for Mastodon.
|
||||
This is the repository for an officially forked Android app for Mastodon.
|
||||
|
||||
Learn more about this app in the [blog post](https://blog.joinmastodon.org/2022/02/official-mastodon-for-android-app-is-coming-soon/).
|
||||
|
||||
## Changes
|
||||
|
||||
* [Enable "Unlisted" as a visibility option](https://github.com/sk22/mastodon-android-fork/tree/feature/enable-unlisted)
|
||||
([Pull request](https://github.com/mastodon/mastodon-android/pull/103)) and
|
||||
[set as default](https://github.com/sk22/mastodon-android-fork/tree/feature/enable-unlisted-as-default)
|
||||
* [Add "Federation" tab and change Discover tab order](https://github.com/sk22/mastodon-android-fork/tree/feature/add-federated-timeline)
|
||||
|
||||
## Building
|
||||
|
||||
As this app is using Java 17 features, you need JDK 17 or newer to build it. Other than that, everything is pretty standard. You can either import the project into Android Studio and build it from there, or run the following command in the project directory:
|
||||
|
||||
@@ -9,8 +9,8 @@ android {
|
||||
applicationId "org.joinmastodon.android"
|
||||
minSdk 23
|
||||
targetSdk 31
|
||||
versionCode 33
|
||||
versionName "1.0.3"
|
||||
versionCode 3
|
||||
versionName '1.0.4-dev+fork.1.1'
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ dependencies {
|
||||
implementation 'me.grishka.litex:dynamicanimation:1.1.0-alpha03'
|
||||
implementation 'me.grishka.litex:viewpager:1.0.0'
|
||||
implementation 'me.grishka.litex:viewpager2:1.0.0'
|
||||
implementation 'me.grishka.appkit:appkit:1.2.2'
|
||||
implementation 'me.grishka.appkit:appkit:1.2.6'
|
||||
implementation 'com.google.code.gson:gson:2.8.9'
|
||||
implementation 'org.jsoup:jsoup:1.14.3'
|
||||
implementation 'com.squareup:otto:1.3.8'
|
||||
|
||||
@@ -14,11 +14,13 @@ import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.model.SearchResult;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
@@ -41,6 +43,8 @@ public class CacheController{
|
||||
private DatabaseHelper db;
|
||||
private final Runnable databaseCloseRunnable=this::closeDatabase;
|
||||
|
||||
private static final int POST_FLAG_GAP_AFTER=1;
|
||||
|
||||
static{
|
||||
databaseThread.start();
|
||||
}
|
||||
@@ -49,14 +53,14 @@ public class CacheController{
|
||||
this.accountID=accountID;
|
||||
}
|
||||
|
||||
public void getHomeTimeline(String maxID, int count, boolean forceReload, Callback<PaginatedResponse<List<Status>>> callback){
|
||||
public void getHomeTimeline(String maxID, int count, boolean forceReload, Callback<CacheablePaginatedResponse<List<Status>>> callback){
|
||||
cancelDelayedClose();
|
||||
databaseThread.postRunnable(()->{
|
||||
try{
|
||||
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
|
||||
if(!forceReload){
|
||||
SQLiteDatabase db=getOrOpenDatabase();
|
||||
try(Cursor cursor=db.query("home_timeline", new String[]{"json"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`id` DESC", count+"")){
|
||||
try(Cursor cursor=db.query("home_timeline", new String[]{"json", "flags"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`id` DESC", count+"")){
|
||||
if(cursor.getCount()==count){
|
||||
ArrayList<Status> result=new ArrayList<>();
|
||||
cursor.moveToFirst();
|
||||
@@ -65,6 +69,8 @@ public class CacheController{
|
||||
do{
|
||||
Status status=MastodonAPIController.gson.fromJson(cursor.getString(0), Status.class);
|
||||
status.postprocess();
|
||||
int flags=cursor.getInt(1);
|
||||
status.hasGapAfter=((flags & POST_FLAG_GAP_AFTER)!=0);
|
||||
newMaxID=status.id;
|
||||
for(Filter filter:filters){
|
||||
if(filter.matches(status.getContentStatus().content))
|
||||
@@ -73,25 +79,18 @@ public class CacheController{
|
||||
result.add(status);
|
||||
}while(cursor.moveToNext());
|
||||
String _newMaxID=newMaxID;
|
||||
uiHandler.post(()->callback.onSuccess(new PaginatedResponse<>(result, _newMaxID)));
|
||||
uiHandler.post(()->callback.onSuccess(new CacheablePaginatedResponse<>(result, _newMaxID, true)));
|
||||
return;
|
||||
}
|
||||
}catch(IOException x){
|
||||
Log.w(TAG, "getHomeTimeline: corrupted status object in database", x);
|
||||
}
|
||||
}
|
||||
new GetHomeTimeline(maxID, null, count)
|
||||
new GetHomeTimeline(maxID, null, count, null)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
callback.onSuccess(new PaginatedResponse<>(result.stream().filter(post->{
|
||||
for(Filter filter:filters){
|
||||
if(filter.matches(post.getContentStatus().content)){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id));
|
||||
callback.onSuccess(new CacheablePaginatedResponse<>(result.stream().filter(new StatusFilterPredicate(filters)).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id, false));
|
||||
putHomeTimeline(result, maxID==null);
|
||||
}
|
||||
|
||||
@@ -110,14 +109,18 @@ public class CacheController{
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private void putHomeTimeline(List<Status> posts, boolean clear){
|
||||
public void putHomeTimeline(List<Status> posts, boolean clear){
|
||||
runOnDbThread((db)->{
|
||||
if(clear)
|
||||
db.delete("home_timeline", null, null);
|
||||
ContentValues values=new ContentValues(2);
|
||||
ContentValues values=new ContentValues(3);
|
||||
for(Status s:posts){
|
||||
values.put("id", s.id);
|
||||
values.put("json", MastodonAPIController.gson.toJson(s));
|
||||
int flags=0;
|
||||
if(s.hasGapAfter)
|
||||
flags|=POST_FLAG_GAP_AFTER;
|
||||
values.put("flags", flags);
|
||||
db.insertWithOnConflict("home_timeline", null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
package org.joinmastodon.android.api;
|
||||
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.FieldNamingPolicy;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonIOException;
|
||||
import com.google.gson.JsonObject;
|
||||
@@ -16,11 +12,9 @@ import com.google.gson.JsonParser;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.api.gson.IsoInstantTypeAdapter;
|
||||
import org.joinmastodon.android.api.gson.IsoLocalDateTypeAdapter;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.model.BaseModel;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
@@ -144,7 +138,7 @@ public class MastodonAPIController{
|
||||
}
|
||||
|
||||
try{
|
||||
req.validateAndPostprocessResponse(respObj);
|
||||
req.validateAndPostprocessResponse(respObj, response);
|
||||
}catch(IOException x){
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error post-processing or validating response", x);
|
||||
|
||||
@@ -28,6 +28,7 @@ import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
private static final String TAG="MastodonAPIRequest";
|
||||
@@ -75,9 +76,14 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
}
|
||||
|
||||
public MastodonAPIRequest<T> exec(String accountID){
|
||||
account=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
domain=account.domain;
|
||||
account.getApiController().submitRequest(this);
|
||||
try{
|
||||
account=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
domain=account.domain;
|
||||
account.getApiController().submitRequest(this);
|
||||
}catch(Exception x){
|
||||
Log.e(TAG, "exec: this shouldn't happen, but it still did", x);
|
||||
invokeErrorCallback(new MastodonErrorResponse(x.getLocalizedMessage(), -1));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -153,7 +159,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
public void validateAndPostprocessResponse(T respObj) throws IOException{
|
||||
public void validateAndPostprocessResponse(T respObj, Response httpResponse) throws IOException{
|
||||
if(respObj instanceof BaseModel){
|
||||
((BaseModel) respObj).postprocess();
|
||||
}else if(respObj instanceof List){
|
||||
|
||||
@@ -121,10 +121,6 @@ public class PushSubscriptionManager{
|
||||
return !TextUtils.isEmpty(deviceToken);
|
||||
}
|
||||
|
||||
public void registerAccountForPush(){
|
||||
registerAccountForPush(null);
|
||||
}
|
||||
|
||||
public void registerAccountForPush(PushSubscription subscription){
|
||||
if(TextUtils.isEmpty(deviceToken))
|
||||
throw new IllegalStateException("No device push token available");
|
||||
@@ -367,7 +363,7 @@ public class PushSubscriptionManager{
|
||||
private static void registerAllAccountsForPush(boolean forceReRegister){
|
||||
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
|
||||
if(session.pushSubscription==null || forceReRegister)
|
||||
session.getPushSubscriptionManager().registerAccountForPush();
|
||||
session.getPushSubscriptionManager().registerAccountForPush(session.pushSubscription);
|
||||
else if(session.needUpdatePushSettings)
|
||||
session.getPushSubscriptionManager().updatePushSettings(session.pushSubscription);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.joinmastodon.android.api.requests;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import okhttp3.Response;
|
||||
|
||||
public abstract class HeaderPaginationRequest<I> extends MastodonAPIRequest<HeaderPaginationList<I>>{
|
||||
private static final Pattern LINK_HEADER_PATTERN=Pattern.compile("(?:(?:,\\s*)?<([^>]+)>|;\\s*(\\w+)=['\"](\\w+)['\"])");
|
||||
|
||||
public HeaderPaginationRequest(HttpMethod method, String path, Class<HeaderPaginationList<I>> respClass){
|
||||
super(method, path, respClass);
|
||||
}
|
||||
|
||||
public HeaderPaginationRequest(HttpMethod method, String path, TypeToken<HeaderPaginationList<I>> respTypeToken){
|
||||
super(method, path, respTypeToken);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateAndPostprocessResponse(HeaderPaginationList<I> respObj, Response httpResponse) throws IOException{
|
||||
super.validateAndPostprocessResponse(respObj, httpResponse);
|
||||
String link=httpResponse.header("Link");
|
||||
if(!TextUtils.isEmpty(link)){
|
||||
Matcher matcher=LINK_HEADER_PATTERN.matcher(link);
|
||||
String url=null;
|
||||
while(matcher.find()){
|
||||
if(url==null){
|
||||
String _url=matcher.group(1);
|
||||
if(_url==null)
|
||||
continue;
|
||||
url=_url;
|
||||
}else{
|
||||
String paramName=matcher.group(2);
|
||||
String paramValue=matcher.group(3);
|
||||
if(paramName==null || paramValue==null)
|
||||
return;
|
||||
if("rel".equals(paramName)){
|
||||
switch(paramValue){
|
||||
case "next" -> respObj.nextPageUri=Uri.parse(url);
|
||||
case "prev" -> respObj.prevPageUri=Uri.parse(url);
|
||||
}
|
||||
url=null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
|
||||
public class GetAccountFollowers extends HeaderPaginationRequest<Account>{
|
||||
public GetAccountFollowers(String id, String maxID, int limit){
|
||||
super(HttpMethod.GET, "/accounts/"+id+"/followers", new TypeToken<>(){});
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", limit+"");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
|
||||
public class GetAccountFollowing extends HeaderPaginationRequest<Account>{
|
||||
public GetAccountFollowing(String id, String maxID, int limit){
|
||||
super(HttpMethod.GET, "/accounts/"+id+"/following", new TypeToken<>(){});
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", limit+"");
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,14 @@ import org.joinmastodon.android.model.Status;
|
||||
import java.util.List;
|
||||
|
||||
public class GetHomeTimeline extends MastodonAPIRequest<List<Status>>{
|
||||
public GetHomeTimeline(String maxID, String minID, int limit){
|
||||
public GetHomeTimeline(String maxID, String minID, int limit, String sinceID){
|
||||
super(HttpMethod.GET, "/timelines/home", new TypeToken<>(){});
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(minID!=null)
|
||||
addQueryParameter("min_id", minID);
|
||||
if(sinceID!=null)
|
||||
addQueryParameter("since_id", sinceID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", ""+limit);
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ public class AccountSessionManager{
|
||||
writeAccountsFile();
|
||||
updateInstanceEmojis(instance, instance.uri);
|
||||
if(PushSubscriptionManager.arePushNotificationsAvailable()){
|
||||
session.getPushSubscriptionManager().registerAccountForPush();
|
||||
session.getPushSubscriptionManager().registerAccountForPush(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
|
||||
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.APIRequest;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseAccountListFragment.AccountItem>{
|
||||
protected HashMap<String, Relationship> relationships=new HashMap<>();
|
||||
protected String accountID;
|
||||
protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>();
|
||||
|
||||
public BaseAccountListFragment(){
|
||||
super(40);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
accountID=getArguments().getString("account");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDataLoaded(List<AccountItem> d, boolean more){
|
||||
if(refreshing){
|
||||
relationships.clear();
|
||||
}
|
||||
loadRelationships(d);
|
||||
super.onDataLoaded(d, more);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRefresh(){
|
||||
for(APIRequest<?> req:relationshipsRequests){
|
||||
req.cancel();
|
||||
}
|
||||
relationshipsRequests.clear();
|
||||
super.onRefresh();
|
||||
}
|
||||
|
||||
protected void loadRelationships(List<AccountItem> accounts){
|
||||
Set<String> ids=accounts.stream().map(ai->ai.account.id).collect(Collectors.toSet());
|
||||
GetAccountRelationships req=new GetAccountRelationships(ids);
|
||||
relationshipsRequests.add(req);
|
||||
req.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Relationship> result){
|
||||
relationshipsRequests.remove(req);
|
||||
for(Relationship rel:result){
|
||||
relationships.put(rel.id, rel);
|
||||
}
|
||||
if(list==null)
|
||||
return;
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
if(list.getChildViewHolder(list.getChildAt(i)) instanceof AccountViewHolder avh){
|
||||
avh.bindRelationship();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
relationshipsRequests.remove(req);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter getAdapter(){
|
||||
return new AccountsAdapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
// list.setPadding(0, V.dp(16), 0, V.dp(16));
|
||||
list.setClipToPadding(false);
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 72, 16));
|
||||
updateToolbar();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig){
|
||||
super.onConfigurationChanged(newConfig);
|
||||
updateToolbar();
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
protected void updateToolbar(){
|
||||
Toolbar toolbar=getToolbar();
|
||||
if(toolbar!=null && toolbar.getNavigationIcon()!=null){
|
||||
toolbar.setNavigationContentDescription(R.string.back);
|
||||
toolbar.setTitleTextAppearance(getActivity(), R.style.m3_title_medium);
|
||||
toolbar.setSubtitleTextAppearance(getActivity(), R.style.m3_body_medium);
|
||||
int color=UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary);
|
||||
toolbar.setTitleTextColor(color);
|
||||
toolbar.setSubtitleTextColor(color);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
|
||||
list.setPadding(0, V.dp(16), 0, V.dp(16)+insets.getSystemWindowInsetBottom());
|
||||
insets=insets.inset(0, 0, 0, insets.getSystemWindowInsetBottom());
|
||||
}else{
|
||||
list.setPadding(0, V.dp(16), 0, V.dp(16));
|
||||
}
|
||||
super.onApplyWindowInsets(insets);
|
||||
}
|
||||
|
||||
protected class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
public AccountsAdapter(){
|
||||
super(imgLoader);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new AccountViewHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(AccountViewHolder holder, int position){
|
||||
holder.bind(data.get(position));
|
||||
super.onBindViewHolder(holder, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return data.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageCountForItem(int position){
|
||||
return data.get(position).emojiHelper.getImageCount()+1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int position, int image){
|
||||
AccountItem item=data.get(position);
|
||||
return image==0 ? item.avaRequest : item.emojiHelper.getImageRequest(image-1);
|
||||
}
|
||||
}
|
||||
|
||||
protected class AccountViewHolder extends BindableViewHolder<AccountItem> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable, UsableRecyclerView.LongClickable{
|
||||
private final TextView name, username;
|
||||
private final ImageView avatar;
|
||||
private final Button button;
|
||||
private final PopupMenu contextMenu;
|
||||
private final View menuAnchor;
|
||||
|
||||
public AccountViewHolder(){
|
||||
super(getActivity(), R.layout.item_account_list, list);
|
||||
name=findViewById(R.id.name);
|
||||
username=findViewById(R.id.username);
|
||||
avatar=findViewById(R.id.avatar);
|
||||
button=findViewById(R.id.button);
|
||||
menuAnchor=findViewById(R.id.menu_anchor);
|
||||
|
||||
avatar.setOutlineProvider(OutlineProviders.roundedRect(12));
|
||||
avatar.setClipToOutline(true);
|
||||
|
||||
button.setOnClickListener(this::onButtonClick);
|
||||
|
||||
contextMenu=new PopupMenu(getActivity(), menuAnchor);
|
||||
contextMenu.inflate(R.menu.profile);
|
||||
contextMenu.setOnMenuItemClickListener(this::onContextMenuItemSelected);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(AccountItem item){
|
||||
name.setText(item.parsedName);
|
||||
username.setText("@"+item.account.acct);
|
||||
bindRelationship();
|
||||
}
|
||||
|
||||
public void bindRelationship(){
|
||||
Relationship rel=relationships.get(item.account.id);
|
||||
if(rel==null){
|
||||
button.setVisibility(View.GONE);
|
||||
}else{
|
||||
button.setVisibility(View.VISIBLE);
|
||||
UiUtils.setRelationshipToActionButton(rel, button);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable image){
|
||||
if(index==0){
|
||||
avatar.setImageDrawable(image);
|
||||
}else{
|
||||
item.emojiHelper.setImageDrawable(index-1, image);
|
||||
name.invalidate();
|
||||
}
|
||||
|
||||
if(image instanceof Animatable a && !a.isRunning())
|
||||
a.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
setImage(index, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(item.account));
|
||||
Nav.go(getActivity(), ProfileFragment.class, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(){
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(float x, float y){
|
||||
Relationship relationship=relationships.get(item.account.id);
|
||||
if(relationship==null)
|
||||
return false;
|
||||
Menu menu=contextMenu.getMenu();
|
||||
Account account=item.account;
|
||||
|
||||
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.mute).setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.block).setTitle(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.getDisplayUsername()));
|
||||
if(relationship.following)
|
||||
menu.findItem(R.id.hide_boosts).setTitle(getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getDisplayUsername()));
|
||||
else
|
||||
menu.findItem(R.id.hide_boosts).setVisible(false);
|
||||
if(!account.isLocal())
|
||||
menu.findItem(R.id.block_domain).setTitle(getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
|
||||
else
|
||||
menu.findItem(R.id.block_domain).setVisible(false);
|
||||
|
||||
menuAnchor.setTranslationX(x);
|
||||
menuAnchor.setTranslationY(y);
|
||||
contextMenu.show();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onButtonClick(View v){
|
||||
ProgressDialog progress=new ProgressDialog(getActivity());
|
||||
progress.setMessage(getString(R.string.loading));
|
||||
progress.setCancelable(false);
|
||||
UiUtils.performAccountAction(getActivity(), item.account, accountID, relationships.get(item.account.id), button, progressShown->{
|
||||
itemView.setHasTransientState(progressShown);
|
||||
if(progressShown)
|
||||
progress.show();
|
||||
else
|
||||
progress.dismiss();
|
||||
}, result->{
|
||||
relationships.put(item.account.id, result);
|
||||
bindRelationship();
|
||||
});
|
||||
}
|
||||
|
||||
private boolean onContextMenuItemSelected(MenuItem item){
|
||||
Relationship relationship=relationships.get(this.item.account.id);
|
||||
if(relationship==null)
|
||||
return false;
|
||||
Account account=this.item.account;
|
||||
|
||||
int id=item.getItemId();
|
||||
if(id==R.id.share){
|
||||
Intent intent=new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
intent.putExtra(Intent.EXTRA_TEXT, account.url);
|
||||
startActivity(Intent.createChooser(intent, item.getTitle()));
|
||||
}else if(id==R.id.mute){
|
||||
UiUtils.confirmToggleMuteUser(getActivity(), accountID, account, relationship.muting, this::updateRelationship);
|
||||
}else if(id==R.id.block){
|
||||
UiUtils.confirmToggleBlockUser(getActivity(), accountID, account, relationship.blocking, this::updateRelationship);
|
||||
}else if(id==R.id.report){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("reportAccount", Parcels.wrap(account));
|
||||
Nav.go(getActivity(), ReportReasonChoiceFragment.class, args);
|
||||
}else if(id==R.id.open_in_browser){
|
||||
UiUtils.launchWebBrowser(getActivity(), account.url);
|
||||
}else if(id==R.id.block_domain){
|
||||
UiUtils.confirmToggleBlockDomain(getActivity(), accountID, account.getDomain(), relationship.domainBlocking, ()->{
|
||||
relationship.domainBlocking=!relationship.domainBlocking;
|
||||
bindRelationship();
|
||||
});
|
||||
}else if(id==R.id.hide_boosts){
|
||||
new SetAccountFollowed(account.id, true, !relationship.showingReblogs)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Relationship result){
|
||||
relationships.put(AccountViewHolder.this.item.account.id, result);
|
||||
bindRelationship();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, false)
|
||||
.exec(accountID);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateRelationship(Relationship r){
|
||||
relationships.put(item.account.id, r);
|
||||
bindRelationship();
|
||||
}
|
||||
}
|
||||
|
||||
protected static class AccountItem{
|
||||
public final Account account;
|
||||
public final ImageLoaderRequest avaRequest;
|
||||
public final CustomEmojiHelper emojiHelper;
|
||||
public final CharSequence parsedName;
|
||||
|
||||
public AccountItem(Account account){
|
||||
this.account=account;
|
||||
avaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(50), V.dp(50));
|
||||
emojiHelper=new CustomEmojiHelper();
|
||||
emojiHelper.setText(parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.PhotoLayoutHelper;
|
||||
import org.joinmastodon.android.ui.TileGridLayoutManager;
|
||||
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.PollFooterStatusDisplayItem;
|
||||
@@ -281,6 +282,10 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
list.getDecoratedBoundsWithMargins(view, outRect);
|
||||
RecyclerView.ViewHolder holder=list.getChildViewHolder(view);
|
||||
if(holder instanceof StatusDisplayItem.Holder){
|
||||
if(((StatusDisplayItem.Holder<?>) holder).getItem().getType()==StatusDisplayItem.Type.GAP){
|
||||
outRect.setEmpty();
|
||||
return;
|
||||
}
|
||||
String id=((StatusDisplayItem.Holder<?>) holder).getItemID();
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
View child=list.getChildAt(i);
|
||||
@@ -474,6 +479,8 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
}
|
||||
}
|
||||
|
||||
public void onGapClick(GapStatusDisplayItem.Holder item){}
|
||||
|
||||
public String getAccountID(){
|
||||
return accountID;
|
||||
}
|
||||
@@ -653,8 +660,8 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
View bottomSibling=parent.getChildAt(i+1);
|
||||
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
|
||||
RecyclerView.ViewHolder siblingHolder=parent.getChildViewHolder(bottomSibling);
|
||||
if(holder instanceof StatusDisplayItem.Holder && siblingHolder instanceof StatusDisplayItem.Holder
|
||||
&& !((StatusDisplayItem.Holder<?>) holder).getItemID().equals(((StatusDisplayItem.Holder<?>) siblingHolder).getItemID())){
|
||||
if(holder instanceof StatusDisplayItem.Holder<?> ih && siblingHolder instanceof StatusDisplayItem.Holder<?> sh
|
||||
&& !ih.getItemID().equals(sh.getItemID()) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP){
|
||||
drawDivider(child, bottomSibling, holder, siblingHolder, parent, c, dividerPaint);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
private ImageView sendError;
|
||||
private View sendingOverlay;
|
||||
private WindowManager wm;
|
||||
private StatusPrivacy statusVisibility=StatusPrivacy.PUBLIC;
|
||||
private StatusPrivacy statusVisibility=StatusPrivacy.UNLISTED;
|
||||
private ComposeAutocompleteSpan currentAutocompleteSpan;
|
||||
private FrameLayout mainEditTextWrap;
|
||||
private ComposeAutocompleteViewController autocompleteViewController;
|
||||
@@ -382,6 +382,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count){
|
||||
if(s.length()==0)
|
||||
return;
|
||||
// offset one char back to catch an already typed '@' or '#' or ':'
|
||||
int realStart=start;
|
||||
start=Math.max(0, start-1);
|
||||
@@ -447,7 +449,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
if(!mentions.contains(m))
|
||||
mentions.add(m);
|
||||
}
|
||||
initialText=TextUtils.join(" ", mentions)+" ";
|
||||
initialText=mentions.isEmpty() ? "" : TextUtils.join(" ", mentions)+" ";
|
||||
if(savedInstanceState==null){
|
||||
mainEditText.setText(initialText);
|
||||
mainEditText.setSelection(mainEditText.length());
|
||||
@@ -714,25 +716,27 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
return false;
|
||||
}
|
||||
String type=getActivity().getContentResolver().getType(uri);
|
||||
if(instance.configuration!=null && instance.configuration.mediaAttachments!=null){
|
||||
if(instance!=null && instance.configuration!=null && instance.configuration.mediaAttachments!=null){
|
||||
if(instance.configuration.mediaAttachments.supportedMimeTypes!=null && !instance.configuration.mediaAttachments.supportedMimeTypes.contains(type)){
|
||||
showMediaAttachmentError(getString(R.string.media_attachment_unsupported_type, UiUtils.getFileName(uri)));
|
||||
return false;
|
||||
}
|
||||
int sizeLimit=type.startsWith("image/") ? instance.configuration.mediaAttachments.imageSizeLimit : instance.configuration.mediaAttachments.videoSizeLimit;
|
||||
int size;
|
||||
try(Cursor cursor=MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.SIZE}, null, null, null)){
|
||||
cursor.moveToFirst();
|
||||
size=cursor.getInt(0);
|
||||
}catch(Exception x){
|
||||
Log.w("ComposeFragment", x);
|
||||
return false;
|
||||
}
|
||||
if(size>sizeLimit){
|
||||
float mb=sizeLimit/(float)(1024*1024);
|
||||
String sMb=String.format(Locale.getDefault(), mb%1f==0f ? "%f" : "%.2f", mb);
|
||||
showMediaAttachmentError(getString(R.string.media_attachment_too_big, UiUtils.getFileName(uri), sMb));
|
||||
return false;
|
||||
if(!type.startsWith("image/")){
|
||||
int sizeLimit=instance.configuration.mediaAttachments.videoSizeLimit;
|
||||
int size;
|
||||
try(Cursor cursor=MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.SIZE}, null, null, null)){
|
||||
cursor.moveToFirst();
|
||||
size=cursor.getInt(0);
|
||||
}catch(Exception x){
|
||||
Log.w("ComposeFragment", x);
|
||||
return false;
|
||||
}
|
||||
if(size>sizeLimit){
|
||||
float mb=sizeLimit/(float) (1024*1024);
|
||||
String sMb=String.format(Locale.getDefault(), mb%1f==0f ? "%f" : "%.2f", mb);
|
||||
showMediaAttachmentError(getString(R.string.media_attachment_too_big, UiUtils.getFileName(uri), sMb));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
pollBtn.setEnabled(false);
|
||||
@@ -1025,7 +1029,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
UiUtils.enablePopupMenuIcons(getActivity(), menu);
|
||||
m.setGroupCheckable(0, true, true);
|
||||
m.findItem(switch(statusVisibility){
|
||||
case PUBLIC, UNLISTED -> R.id.vis_public;
|
||||
case PUBLIC -> R.id.vis_public;
|
||||
case UNLISTED -> R.id.vis_unlisted;
|
||||
case PRIVATE -> R.id.vis_followers;
|
||||
case DIRECT -> R.id.vis_private;
|
||||
}).setChecked(true);
|
||||
@@ -1035,6 +1040,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
int id=item.getItemId();
|
||||
if(id==R.id.vis_public){
|
||||
statusVisibility=StatusPrivacy.PUBLIC;
|
||||
}else if(id==R.id.vis_unlisted){
|
||||
statusVisibility=StatusPrivacy.UNLISTED;
|
||||
}else if(id==R.id.vis_followers){
|
||||
statusVisibility=StatusPrivacy.PRIVATE;
|
||||
}else if(id==R.id.vis_private){
|
||||
@@ -1062,7 +1069,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
|
||||
@Override
|
||||
public void onSelectionChanged(int start, int end){
|
||||
if(start==end){
|
||||
if(start==end && mainEditText.length()>0){
|
||||
ComposeAutocompleteSpan[] spans=mainEditText.getText().getSpans(start, end, ComposeAutocompleteSpan.class);
|
||||
if(spans.length>0){
|
||||
assert spans.length==1;
|
||||
@@ -1096,7 +1103,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
|
||||
@Override
|
||||
public String[] onGetAllowedMediaMimeTypes(){
|
||||
if(instance.configuration!=null && instance.configuration.mediaAttachments!=null && instance.configuration.mediaAttachments.supportedMimeTypes!=null)
|
||||
if(instance!=null && instance.configuration!=null && instance.configuration.mediaAttachments!=null && instance.configuration.mediaAttachments.supportedMimeTypes!=null)
|
||||
return instance.configuration.mediaAttachments.supportedMimeTypes.toArray(new String[0]);
|
||||
return new String[]{"image/jpeg", "image/gif", "image/png", "video/mp4"};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountFollowers;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class FollowerListFragment extends BaseAccountListFragment{
|
||||
private Account account;
|
||||
private String nextMaxID;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
|
||||
setTitle("@"+account.acct);
|
||||
setSubtitle(getResources().getQuantityString(R.plurals.x_followers, account.followersCount, account.followersCount));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume(){
|
||||
super.onResume();
|
||||
if(!loaded && !dataLoading)
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetAccountFollowers(account.id, offset==0 ? null : nextMaxID, count)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Account> result){
|
||||
if(result.nextPageUri!=null)
|
||||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||
else
|
||||
nextMaxID=null;
|
||||
onDataLoaded(result.stream().map(AccountItem::new).collect(Collectors.toList()), nextMaxID!=null);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountFollowing;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class FollowingListFragment extends BaseAccountListFragment{
|
||||
private Account account;
|
||||
private String nextMaxID;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
|
||||
setTitle("@"+account.acct);
|
||||
setSubtitle(getResources().getQuantityString(R.plurals.x_following, account.followingCount, account.followingCount));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume(){
|
||||
super.onResume();
|
||||
if(!loaded && !dataLoading)
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetAccountFollowing(account.id, offset==0 ? null : nextMaxID, count)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Account> result){
|
||||
if(result.nextPageUri!=null)
|
||||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||
else
|
||||
nextMaxID=null;
|
||||
onDataLoaded(result.stream().map(AccountItem::new).collect(Collectors.toList()), nextMaxID!=null);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import android.app.NotificationManager;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Outline;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
@@ -17,6 +18,7 @@ import android.view.WindowInsets;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
@@ -28,6 +30,7 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.discover.DiscoverFragment;
|
||||
import org.joinmastodon.android.fragments.discover.SearchFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.TabBar;
|
||||
@@ -46,6 +49,7 @@ import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.BottomSheet;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
|
||||
public class HomeFragment extends AppKitFragment implements OnBackPressedListener{
|
||||
@@ -238,21 +242,11 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
|
||||
private boolean onTabLongClick(@IdRes int tab){
|
||||
if(tab==R.id.tab_profile){
|
||||
ArrayList<String> options=new ArrayList<>();
|
||||
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
|
||||
options.add(session.self.displayName+"\n("+session.self.username+"@"+session.domain+")");
|
||||
}
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setItems(options.toArray(new String[0]), (dialog, which)->{
|
||||
AccountSession session=AccountSessionManager.getInstance().getLoggedInAccounts().get(which);
|
||||
AccountSessionManager.getInstance().setLastActiveAccountID(session.getID());
|
||||
getActivity().finish();
|
||||
getActivity().startActivity(new Intent(getActivity(), MainActivity.class));
|
||||
})
|
||||
.setNegativeButton(R.string.add_account, (dialog, which)->{
|
||||
Nav.go(getActivity(), SplashFragment.class, null);
|
||||
})
|
||||
.show();
|
||||
ArrayList<String> options=new ArrayList<>();
|
||||
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
|
||||
options.add(session.self.displayName+"\n("+session.self.username+"@"+session.domain+")");
|
||||
}
|
||||
new AccountSwitcherSheet(getActivity()).show();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.app.Activity;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.Gravity;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Toolbar;
|
||||
@@ -16,20 +24,38 @@ import android.widget.Toolbar;
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class HomeTimelineFragment extends StatusListFragment{
|
||||
private ImageButton fab;
|
||||
private ImageView toolbarLogo;
|
||||
private Button toolbarShowNewPostsBtn;
|
||||
private boolean newPostsBtnShown;
|
||||
private AnimatorSet currentNewPostsAnim;
|
||||
|
||||
private String maxID;
|
||||
|
||||
@@ -50,11 +76,13 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
.getAccount(accountID).getCacheController()
|
||||
.getHomeTimeline(offset>0 ? maxID : null, count, refreshing, new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(PaginatedResponse<List<Status>> result){
|
||||
public void onSuccess(CacheablePaginatedResponse<List<Status>> result){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
onDataLoaded(result.items, !result.items.isEmpty());
|
||||
maxID=result.maxID;
|
||||
if(result.isFromCache())
|
||||
loadNewPosts();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -65,6 +93,14 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
fab=view.findViewById(R.id.fab);
|
||||
fab.setOnClickListener(this::onFabClick);
|
||||
updateToolbarLogo();
|
||||
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
|
||||
if(newPostsBtnShown && list.getChildAdapterPosition(list.getChildAt(0))<=getMainAdapterOffset()){
|
||||
hideNewPostsButton();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -89,8 +125,13 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
|
||||
loadData();
|
||||
if(!getArguments().getBoolean("noAutoLoad")){
|
||||
if(!loaded && !dataLoading){
|
||||
loadData();
|
||||
}else if(!dataLoading){
|
||||
loadNewPosts();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
@@ -104,12 +145,256 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
Nav.go(getActivity(), ComposeFragment.class, args);
|
||||
}
|
||||
|
||||
private void loadNewPosts(){
|
||||
dataLoading=true;
|
||||
// The idea here is that we request the timeline such that if there are fewer than `limit` posts,
|
||||
// we'll get the currently topmost post as last in the response. This way we know there's no gap
|
||||
// between the existing and newly loaded parts of the timeline.
|
||||
String sinceID=data.size()>1 ? data.get(1).id : "1";
|
||||
currentRequest=new GetHomeTimeline(null, null, 20, sinceID)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
currentRequest=null;
|
||||
dataLoading=false;
|
||||
if(result.isEmpty() || getActivity()==null)
|
||||
return;
|
||||
Status last=result.get(result.size()-1);
|
||||
List<Status> toAdd;
|
||||
if(last.id.equals(data.get(0).id)){ // This part intersects with the existing one
|
||||
toAdd=result.subList(0, result.size()-1); // Remove the already known last post
|
||||
}else{
|
||||
result.get(result.size()-1).hasGapAfter=true;
|
||||
toAdd=result;
|
||||
}
|
||||
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
|
||||
if(!filters.isEmpty()){
|
||||
toAdd=toAdd.stream().filter(new StatusFilterPredicate(filters)).collect(Collectors.toList());
|
||||
}
|
||||
if(!toAdd.isEmpty()){
|
||||
prependItems(toAdd, true);
|
||||
showNewPostsButton();
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(toAdd, false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
currentRequest=null;
|
||||
dataLoading=false;
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGapClick(GapStatusDisplayItem.Holder item){
|
||||
if(dataLoading)
|
||||
return;
|
||||
item.getItem().loading=true;
|
||||
V.setVisibilityAnimated(item.progress, View.VISIBLE);
|
||||
V.setVisibilityAnimated(item.text, View.GONE);
|
||||
GapStatusDisplayItem gap=item.getItem();
|
||||
dataLoading=true;
|
||||
currentRequest=new GetHomeTimeline(item.getItemID(), null, 20, null)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
currentRequest=null;
|
||||
dataLoading=false;
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
int gapPos=displayItems.indexOf(gap);
|
||||
if(gapPos==-1)
|
||||
return;
|
||||
if(result.isEmpty()){
|
||||
displayItems.remove(gapPos);
|
||||
adapter.notifyItemRemoved(getMainAdapterOffset()+gapPos);
|
||||
Status gapStatus=getStatusByID(gap.parentID);
|
||||
if(gapStatus!=null){
|
||||
gapStatus.hasGapAfter=false;
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(gapStatus), false);
|
||||
}
|
||||
}else{
|
||||
Set<String> idsBelowGap=new HashSet<>();
|
||||
boolean belowGap=false;
|
||||
int gapPostIndex=0;
|
||||
for(Status s:data){
|
||||
if(belowGap){
|
||||
idsBelowGap.add(s.id);
|
||||
}else if(s.id.equals(gap.parentID)){
|
||||
belowGap=true;
|
||||
s.hasGapAfter=false;
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(s), false);
|
||||
}else{
|
||||
gapPostIndex++;
|
||||
}
|
||||
}
|
||||
int endIndex=0;
|
||||
for(Status s:result){
|
||||
endIndex++;
|
||||
if(idsBelowGap.contains(s.id))
|
||||
break;
|
||||
}
|
||||
if(endIndex==result.size()){
|
||||
result.get(result.size()-1).hasGapAfter=true;
|
||||
}else{
|
||||
result=result.subList(0, endIndex);
|
||||
}
|
||||
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1);
|
||||
targetList.clear();
|
||||
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
|
||||
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
|
||||
outer:
|
||||
for(Status s:result){
|
||||
if(idsBelowGap.contains(s.id))
|
||||
break;
|
||||
for(Filter filter:filters){
|
||||
if(filter.matches(s.getContentStatus().content)){
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
targetList.addAll(buildDisplayItems(s));
|
||||
insertedPosts.add(s);
|
||||
}
|
||||
if(targetList.isEmpty()){
|
||||
// oops. We didn't add new posts, but at least we know there are none.
|
||||
adapter.notifyItemRemoved(getMainAdapterOffset()+gapPos);
|
||||
}else{
|
||||
adapter.notifyItemChanged(getMainAdapterOffset()+gapPos);
|
||||
adapter.notifyItemRangeInserted(getMainAdapterOffset()+gapPos+1, targetList.size()-1);
|
||||
}
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(insertedPosts, false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
currentRequest=null;
|
||||
dataLoading=false;
|
||||
gap.loading=false;
|
||||
Activity a=getActivity();
|
||||
if(a!=null){
|
||||
error.showToast(a);
|
||||
int gapPos=displayItems.indexOf(gap);
|
||||
if(gapPos>=0)
|
||||
adapter.notifyItemChanged(gapPos);
|
||||
}
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRefresh(){
|
||||
if(currentRequest!=null){
|
||||
currentRequest.cancel();
|
||||
currentRequest=null;
|
||||
dataLoading=false;
|
||||
}
|
||||
super.onRefresh();
|
||||
}
|
||||
|
||||
private void updateToolbarLogo(){
|
||||
ImageView logo=new ImageView(getActivity());
|
||||
logo.setScaleType(ImageView.ScaleType.CENTER);
|
||||
logo.setImageResource(R.drawable.logo);
|
||||
logo.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary)));
|
||||
toolbarLogo=new ImageView(getActivity());
|
||||
toolbarLogo.setScaleType(ImageView.ScaleType.CENTER);
|
||||
toolbarLogo.setImageResource(R.drawable.logo);
|
||||
toolbarLogo.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary)));
|
||||
|
||||
toolbarShowNewPostsBtn=new Button(getActivity());
|
||||
toolbarShowNewPostsBtn.setTextAppearance(R.style.m3_title_medium);
|
||||
toolbarShowNewPostsBtn.setTextColor(0xffffffff);
|
||||
toolbarShowNewPostsBtn.setStateListAnimator(null);
|
||||
toolbarShowNewPostsBtn.setBackgroundResource(R.drawable.bg_button_new_posts);
|
||||
toolbarShowNewPostsBtn.setText(R.string.see_new_posts);
|
||||
toolbarShowNewPostsBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_fluent_arrow_up_16_filled, 0, 0, 0);
|
||||
toolbarShowNewPostsBtn.setCompoundDrawableTintList(toolbarShowNewPostsBtn.getTextColors());
|
||||
toolbarShowNewPostsBtn.setCompoundDrawablePadding(V.dp(8));
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N)
|
||||
UiUtils.fixCompoundDrawableTintOnAndroid6(toolbarShowNewPostsBtn);
|
||||
toolbarShowNewPostsBtn.setOnClickListener(this::onNewPostsBtnClick);
|
||||
|
||||
if(newPostsBtnShown){
|
||||
toolbarShowNewPostsBtn.setVisibility(View.VISIBLE);
|
||||
toolbarLogo.setVisibility(View.INVISIBLE);
|
||||
toolbarLogo.setAlpha(0f);
|
||||
}else{
|
||||
toolbarShowNewPostsBtn.setVisibility(View.INVISIBLE);
|
||||
toolbarShowNewPostsBtn.setAlpha(0f);
|
||||
toolbarShowNewPostsBtn.setScaleX(.8f);
|
||||
toolbarShowNewPostsBtn.setScaleY(.8f);
|
||||
toolbarLogo.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
FrameLayout logoWrap=new FrameLayout(getActivity());
|
||||
logoWrap.addView(toolbarLogo, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
|
||||
logoWrap.addView(toolbarShowNewPostsBtn, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, V.dp(32), Gravity.CENTER));
|
||||
|
||||
Toolbar toolbar=getToolbar();
|
||||
toolbar.addView(logo, new Toolbar.LayoutParams(Gravity.CENTER));
|
||||
toolbar.addView(logoWrap, new Toolbar.LayoutParams(Gravity.CENTER));
|
||||
}
|
||||
|
||||
private void showNewPostsButton(){
|
||||
if(newPostsBtnShown)
|
||||
return;
|
||||
newPostsBtnShown=true;
|
||||
if(currentNewPostsAnim!=null){
|
||||
currentNewPostsAnim.cancel();
|
||||
}
|
||||
toolbarShowNewPostsBtn.setVisibility(View.VISIBLE);
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
set.playTogether(
|
||||
ObjectAnimator.ofFloat(toolbarLogo, View.ALPHA, 0f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.ALPHA, 1f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_X, 1f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_Y, 1f)
|
||||
);
|
||||
set.setDuration(300);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
toolbarLogo.setVisibility(View.INVISIBLE);
|
||||
currentNewPostsAnim=null;
|
||||
}
|
||||
});
|
||||
currentNewPostsAnim=set;
|
||||
set.start();
|
||||
}
|
||||
|
||||
private void hideNewPostsButton(){
|
||||
if(!newPostsBtnShown)
|
||||
return;
|
||||
newPostsBtnShown=false;
|
||||
if(currentNewPostsAnim!=null){
|
||||
currentNewPostsAnim.cancel();
|
||||
}
|
||||
toolbarLogo.setVisibility(View.VISIBLE);
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
set.playTogether(
|
||||
ObjectAnimator.ofFloat(toolbarLogo, View.ALPHA, 1f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.ALPHA, 0f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_X, .8f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_Y, .8f)
|
||||
);
|
||||
set.setDuration(300);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
toolbarShowNewPostsBtn.setVisibility(View.INVISIBLE);
|
||||
currentNewPostsAnim=null;
|
||||
}
|
||||
});
|
||||
currentNewPostsAnim=set;
|
||||
set.start();
|
||||
}
|
||||
|
||||
private void onNewPostsBtnClick(View v){
|
||||
if(newPostsBtnShown){
|
||||
hideNewPostsButton();
|
||||
scrollToTop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.ImageSpan;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
@@ -104,6 +106,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
private ProgressBar actionProgress;
|
||||
private FrameLayout[] tabViews;
|
||||
private TabLayoutMediator tabLayoutMediator;
|
||||
private TextView followsYouView;
|
||||
|
||||
private Account account;
|
||||
private String accountID;
|
||||
@@ -178,6 +181,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
bioEdit=content.findViewById(R.id.bio_edit);
|
||||
actionProgress=content.findViewById(R.id.action_progress);
|
||||
fab=content.findViewById(R.id.fab);
|
||||
followsYouView=content.findViewById(R.id.follows_you);
|
||||
|
||||
avatar.setOutlineProvider(new ViewOutlineProvider(){
|
||||
@Override
|
||||
@@ -257,6 +261,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
fab.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
followersBtn.setOnClickListener(this::onFollowersOrFollowingClick);
|
||||
followingBtn.setOnClickListener(this::onFollowersOrFollowingClick);
|
||||
|
||||
return sizeWrapper;
|
||||
}
|
||||
|
||||
@@ -400,8 +407,25 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
HtmlParser.parseCustomEmoji(ssb, account.emojis);
|
||||
name.setText(ssb);
|
||||
setTitle(ssb);
|
||||
username.setText('@'+account.acct);
|
||||
bio.setText(HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID));
|
||||
if(account.locked){
|
||||
ssb=new SpannableStringBuilder("@");
|
||||
ssb.append(account.acct);
|
||||
ssb.append(" ");
|
||||
Drawable lock=username.getResources().getDrawable(R.drawable.ic_fluent_lock_closed_20_filled, getActivity().getTheme()).mutate();
|
||||
lock.setBounds(0, 0, lock.getIntrinsicWidth(), lock.getIntrinsicHeight());
|
||||
lock.setTint(username.getCurrentTextColor());
|
||||
ssb.append(getString(R.string.manually_approves_followers), new ImageSpan(lock, ImageSpan.ALIGN_BOTTOM), 0);
|
||||
username.setText(ssb);
|
||||
}else{
|
||||
username.setText('@'+account.acct);
|
||||
}
|
||||
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
|
||||
if(TextUtils.isEmpty(parsedBio)){
|
||||
bio.setVisibility(View.GONE);
|
||||
}else{
|
||||
bio.setVisibility(View.VISIBLE);
|
||||
bio.setText(parsedBio);
|
||||
}
|
||||
followersCount.setText(UiUtils.abbreviateNumber(account.followersCount));
|
||||
followingCount.setText(UiUtils.abbreviateNumber(account.followingCount));
|
||||
postsCount.setText(UiUtils.abbreviateNumber(account.statusesCount));
|
||||
@@ -565,6 +589,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
invalidateOptionsMenu();
|
||||
actionButton.setVisibility(View.VISIBLE);
|
||||
UiUtils.setRelationshipToActionButton(relationship, actionButton);
|
||||
actionProgress.setIndeterminateTintList(actionButton.getTextColors());
|
||||
followsYouView.setVisibility(relationship.followedBy ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
private void onScrollChanged(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){
|
||||
@@ -825,6 +851,20 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
scrollView.smoothScrollTo(0, 0);
|
||||
}
|
||||
|
||||
private void onFollowersOrFollowingClick(View v){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("targetAccount", Parcels.wrap(account));
|
||||
Class<? extends Fragment> cls;
|
||||
if(v.getId()==R.id.followers_btn)
|
||||
cls=FollowerListFragment.class;
|
||||
else if(v.getId()==R.id.following_btn)
|
||||
cls=FollowingListFragment.class;
|
||||
else
|
||||
return;
|
||||
Nav.go(getActivity(), cls, args);
|
||||
}
|
||||
|
||||
private class ProfilePagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
|
||||
@NonNull
|
||||
@Override
|
||||
|
||||
@@ -51,6 +51,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
private DiscoverAccountsFragment accountsFragment;
|
||||
private SearchFragment searchFragment;
|
||||
private LocalTimelineFragment localTimelineFragment;
|
||||
private FederatedTimelineFragment federatedTimelineFragment;
|
||||
|
||||
private String accountID;
|
||||
private Runnable searchDebouncer=this::onSearchChangedDebounced;
|
||||
@@ -72,15 +73,16 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
tabLayout=view.findViewById(R.id.tabbar);
|
||||
pager=view.findViewById(R.id.pager);
|
||||
|
||||
tabViews=new FrameLayout[5];
|
||||
tabViews=new FrameLayout[6];
|
||||
for(int i=0;i<tabViews.length;i++){
|
||||
FrameLayout tabView=new FrameLayout(getActivity());
|
||||
tabView.setId(switch(i){
|
||||
case 0 -> R.id.discover_posts;
|
||||
case 1 -> R.id.discover_hashtags;
|
||||
case 2 -> R.id.discover_news;
|
||||
case 3 -> R.id.discover_local_timeline;
|
||||
case 4 -> R.id.discover_users;
|
||||
case 0 -> R.id.discover_local_timeline;
|
||||
case 1 -> R.id.discover_federated_timeline;
|
||||
case 2 -> R.id.discover_hashtags;
|
||||
case 3 -> R.id.discover_posts;
|
||||
case 4 -> R.id.discover_news;
|
||||
case 5 -> R.id.discover_users;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+i);
|
||||
});
|
||||
tabView.setVisibility(View.GONE);
|
||||
@@ -106,7 +108,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
}
|
||||
});
|
||||
|
||||
if(postsFragment==null){
|
||||
if(localTimelineFragment==null){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putBoolean("__is_tab", true);
|
||||
@@ -126,9 +128,13 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
localTimelineFragment=new LocalTimelineFragment();
|
||||
localTimelineFragment.setArguments(args);
|
||||
|
||||
federatedTimelineFragment=new FederatedTimelineFragment();
|
||||
federatedTimelineFragment.setArguments(args);
|
||||
|
||||
getChildFragmentManager().beginTransaction()
|
||||
.add(R.id.discover_posts, postsFragment)
|
||||
.add(R.id.discover_local_timeline, localTimelineFragment)
|
||||
.add(R.id.discover_federated_timeline, federatedTimelineFragment)
|
||||
.add(R.id.discover_hashtags, hashtagsFragment)
|
||||
.add(R.id.discover_news, newsFragment)
|
||||
.add(R.id.discover_users, accountsFragment)
|
||||
@@ -139,11 +145,12 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
@Override
|
||||
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){
|
||||
tab.setText(switch(position){
|
||||
case 0 -> R.string.posts;
|
||||
case 1 -> R.string.hashtags;
|
||||
case 2 -> R.string.news;
|
||||
case 3 -> R.string.local_timeline;
|
||||
case 4 -> R.string.for_you;
|
||||
case 0 -> R.string.local_timeline;
|
||||
case 1 -> R.string.federated_timeline;
|
||||
case 2 -> R.string.hashtags;
|
||||
case 3 -> R.string.posts;
|
||||
case 4 -> R.string.news;
|
||||
case 5 -> R.string.for_you;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+position);
|
||||
});
|
||||
tab.view.textView.setAllCaps(true);
|
||||
@@ -217,8 +224,8 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
}
|
||||
|
||||
public void loadData(){
|
||||
if(postsFragment!=null && !postsFragment.loaded && !postsFragment.dataLoading)
|
||||
postsFragment.loadData();
|
||||
if(localTimelineFragment!=null && !localTimelineFragment.loaded && !localTimelineFragment.dataLoading)
|
||||
localTimelineFragment.loadData();
|
||||
}
|
||||
|
||||
private void onSearchEditFocusChanged(View v, boolean hasFocus){
|
||||
@@ -254,11 +261,12 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
|
||||
private Fragment getFragmentForPage(int page){
|
||||
return switch(page){
|
||||
case 0 -> postsFragment;
|
||||
case 1 -> hashtagsFragment;
|
||||
case 2 -> newsFragment;
|
||||
case 3 -> localTimelineFragment;
|
||||
case 4 -> accountsFragment;
|
||||
case 0 -> localTimelineFragment;
|
||||
case 1 -> federatedTimelineFragment;
|
||||
case 2 -> hashtagsFragment;
|
||||
case 3 -> postsFragment;
|
||||
case 4 -> newsFragment;
|
||||
case 5 -> accountsFragment;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+page);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.joinmastodon.android.fragments.discover;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
|
||||
import org.joinmastodon.android.fragments.StatusListFragment;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class FederatedTimelineFragment extends StatusListFragment{
|
||||
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.FEDERATED_TIMELINE);
|
||||
private String maxID;
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetPublicTimeline(false, false, refreshing ? null : maxID, count)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
if(!result.isEmpty())
|
||||
maxID=result.get(result.size()-1).id;
|
||||
onDataLoaded(result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList()), !result.isEmpty());
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
bannerHelper.maybeAddBanner(contentWrap);
|
||||
}
|
||||
}
|
||||
@@ -5,23 +5,29 @@ import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
|
||||
import org.joinmastodon.android.fragments.StatusListFragment;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class LocalTimelineFragment extends StatusListFragment{
|
||||
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.LOCAL_TIMELINE);
|
||||
private String maxID;
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetPublicTimeline(true, false, refreshing ? null : getMaxID(), count)
|
||||
currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, count)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
if(!result.isEmpty())
|
||||
maxID=result.get(result.size()-1).id;
|
||||
onDataLoaded(result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList()), !result.isEmpty());
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
|
||||
@@ -14,6 +14,7 @@ import android.view.WindowInsets;
|
||||
import android.widget.Button;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
|
||||
@@ -136,6 +137,13 @@ public class AccountActivationFragment extends AppKitFragment{
|
||||
}
|
||||
|
||||
private void tryGetAccount(){
|
||||
if(AccountSessionManager.getInstance().tryGetAccount(accountID)==null){
|
||||
uiHandler.removeCallbacks(pollRunnable);
|
||||
getActivity().finish();
|
||||
Intent intent=new Intent(getActivity(), MainActivity.class);
|
||||
startActivity(intent);
|
||||
return;
|
||||
}
|
||||
currentRequest=new GetOwnAccount()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
|
||||
@@ -15,6 +15,7 @@ import android.widget.TextView;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
|
||||
import org.joinmastodon.android.events.FinishReportFragmentsEvent;
|
||||
@@ -60,6 +61,13 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
|
||||
setRetainInstance(true);
|
||||
setListLayoutId(R.layout.fragment_content_report_posts);
|
||||
setLayout(R.layout.fragment_report_posts);
|
||||
E.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
E.unregister(this);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -5,7 +5,9 @@ import android.os.Bundle;
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.FinishReportFragmentsEvent;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.ReportReason;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
@@ -21,7 +23,10 @@ public class ReportReasonChoiceFragment extends BaseReportChoiceFragment{
|
||||
protected void populateItems(){
|
||||
items.add(new Item(getString(R.string.report_reason_personal), getString(R.string.report_reason_personal_subtitle), ReportReason.PERSONAL.name()));
|
||||
items.add(new Item(getString(R.string.report_reason_spam), getString(R.string.report_reason_spam_subtitle), ReportReason.SPAM.name()));
|
||||
items.add(new Item(getString(R.string.report_reason_violation), getString(R.string.report_reason_violation_subtitle), ReportReason.VIOLATION.name()));
|
||||
Instance inst=AccountSessionManager.getInstance().getInstanceInfo(AccountSessionManager.getInstance().getAccount(accountID).domain);
|
||||
if(inst!=null && inst.rules!=null && !inst.rules.isEmpty()){
|
||||
items.add(new Item(getString(R.string.report_reason_violation), getString(R.string.report_reason_violation_subtitle), ReportReason.VIOLATION.name()));
|
||||
}
|
||||
items.add(new Item(getString(R.string.report_reason_other), getString(R.string.report_reason_other_subtitle), ReportReason.OTHER.name()));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.joinmastodon.android.model;
|
||||
|
||||
public class CacheablePaginatedResponse<T> extends PaginatedResponse<T>{
|
||||
private final boolean fromCache;
|
||||
|
||||
public CacheablePaginatedResponse(T items, String maxID, boolean fromCache){
|
||||
super(items, maxID);
|
||||
this.fromCache=fromCache;
|
||||
}
|
||||
|
||||
public boolean isFromCache(){
|
||||
return fromCache;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.joinmastodon.android.model;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class HeaderPaginationList<T> extends ArrayList<T>{
|
||||
public Uri nextPageUri, prevPageUri;
|
||||
|
||||
public HeaderPaginationList(int initialCapacity){
|
||||
super(initialCapacity);
|
||||
}
|
||||
|
||||
public HeaderPaginationList(){
|
||||
super();
|
||||
}
|
||||
|
||||
public HeaderPaginationList(@NonNull Collection<? extends T> c){
|
||||
super(c);
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,7 @@ public class Status extends BaseModel implements DisplayItemsParent{
|
||||
public boolean pinned;
|
||||
|
||||
public transient boolean spoilerRevealed;
|
||||
public transient boolean hasGapAfter;
|
||||
|
||||
@Override
|
||||
public void postprocess() throws ObjectValidationException{
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
package org.joinmastodon.android.ui;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.SplashFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.ListImageLoaderWrapper;
|
||||
import me.grishka.appkit.imageloader.RecyclerViewDelegate;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.BottomSheet;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class AccountSwitcherSheet extends BottomSheet{
|
||||
private final Activity activity;
|
||||
private UsableRecyclerView list;
|
||||
private List<WrappedAccount> accounts;
|
||||
private ListImageLoaderWrapper imgLoader;
|
||||
|
||||
public AccountSwitcherSheet(@NonNull Activity activity){
|
||||
super(activity);
|
||||
this.activity=activity;
|
||||
|
||||
accounts=AccountSessionManager.getInstance().getLoggedInAccounts().stream().map(WrappedAccount::new).collect(Collectors.toList());
|
||||
|
||||
list=new UsableRecyclerView(activity);
|
||||
imgLoader=new ListImageLoaderWrapper(activity, list, new RecyclerViewDelegate(list), null);
|
||||
list.setClipToPadding(false);
|
||||
list.setLayoutManager(new LinearLayoutManager(activity));
|
||||
|
||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
View handle=new View(activity);
|
||||
handle.setBackgroundResource(R.drawable.bg_bottom_sheet_handle);
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(handle));
|
||||
adapter.addAdapter(new AccountsAdapter());
|
||||
AccountViewHolder holder=new AccountViewHolder();
|
||||
holder.more.setVisibility(View.GONE);
|
||||
holder.currentIcon.setVisibility(View.GONE);
|
||||
holder.name.setText(R.string.add_account);
|
||||
holder.avatar.setScaleType(ImageView.ScaleType.CENTER);
|
||||
holder.avatar.setImageResource(R.drawable.ic_fluent_add_circle_24_filled);
|
||||
holder.avatar.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(activity, android.R.attr.textColorPrimary)));
|
||||
adapter.addAdapter(new ClickableSingleViewRecyclerAdapter(holder.itemView, ()->{
|
||||
Nav.go(activity, SplashFragment.class, null);
|
||||
dismiss();
|
||||
}));
|
||||
|
||||
list.setAdapter(adapter);
|
||||
DividerItemDecoration divider=new DividerItemDecoration(activity, R.attr.colorPollVoted, .5f, 72, 16, DividerItemDecoration.NOT_FIRST);
|
||||
divider.setDrawBelowLastItem(true);
|
||||
list.addItemDecoration(divider);
|
||||
|
||||
FrameLayout content=new FrameLayout(activity);
|
||||
content.setBackgroundResource(R.drawable.bg_bottom_sheet);
|
||||
content.addView(list);
|
||||
setContentView(content);
|
||||
setNavigationBarBackground(new ColorDrawable(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground)), !UiUtils.isDarkTheme());
|
||||
}
|
||||
|
||||
private void confirmLogOut(String accountID){
|
||||
new M3AlertDialogBuilder(activity)
|
||||
.setTitle(R.string.log_out)
|
||||
.setMessage(R.string.confirm_log_out)
|
||||
.setPositiveButton(R.string.log_out, (dialog, which) -> logOut(accountID))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void logOut(String accountID){
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
new RevokeOauthToken(session.app.clientId, session.app.clientSecret, session.token.accessToken)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Object result){
|
||||
onLoggedOut(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
onLoggedOut(accountID);
|
||||
}
|
||||
})
|
||||
.wrapProgress(activity, R.string.loading, false)
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void onLoggedOut(String accountID){
|
||||
AccountSessionManager.getInstance().removeAccount(accountID);
|
||||
dismiss();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onWindowInsetsUpdated(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=29){
|
||||
int tappableBottom=insets.getTappableElementInsets().bottom;
|
||||
int insetBottom=insets.getSystemWindowInsetBottom();
|
||||
if(tappableBottom==0 && insetBottom>0){
|
||||
list.setPadding(0, 0, 0, V.dp(48)-insetBottom);
|
||||
}else{
|
||||
list.setPadding(0, 0, 0, V.dp(24));
|
||||
}
|
||||
}else{
|
||||
list.setPadding(0, 0, 0, V.dp(24));
|
||||
}
|
||||
}
|
||||
|
||||
private class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
public AccountsAdapter(){
|
||||
super(imgLoader);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new AccountViewHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return accounts.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(AccountViewHolder holder, int position){
|
||||
holder.bind(accounts.get(position).session);
|
||||
super.onBindViewHolder(holder, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageCountForItem(int position){
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int position, int image){
|
||||
return accounts.get(position).req;
|
||||
}
|
||||
}
|
||||
|
||||
private class AccountViewHolder extends BindableViewHolder<AccountSession> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
|
||||
private final TextView name;
|
||||
private final ImageView avatar;
|
||||
private final ImageButton more;
|
||||
private final View currentIcon;
|
||||
private final PopupMenu menu;
|
||||
|
||||
public AccountViewHolder(){
|
||||
super(activity, R.layout.item_account_switcher, list);
|
||||
name=findViewById(R.id.name);
|
||||
avatar=findViewById(R.id.avatar);
|
||||
more=findViewById(R.id.more);
|
||||
currentIcon=findViewById(R.id.current);
|
||||
|
||||
avatar.setOutlineProvider(OutlineProviders.roundedRect(12));
|
||||
avatar.setClipToOutline(true);
|
||||
|
||||
menu=new PopupMenu(activity, more);
|
||||
menu.inflate(R.menu.account_switcher);
|
||||
menu.setOnMenuItemClickListener(item1 -> {
|
||||
confirmLogOut(item.getID());
|
||||
return true;
|
||||
});
|
||||
more.setOnClickListener(v->menu.show());
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
@Override
|
||||
public void onBind(AccountSession item){
|
||||
name.setText("@"+item.self.username+"@"+item.domain);
|
||||
if(AccountSessionManager.getInstance().getLastActiveAccountID().equals(item.getID())){
|
||||
more.setVisibility(View.GONE);
|
||||
currentIcon.setVisibility(View.VISIBLE);
|
||||
}else{
|
||||
more.setVisibility(View.VISIBLE);
|
||||
currentIcon.setVisibility(View.GONE);
|
||||
}
|
||||
menu.getMenu().findItem(R.id.log_out).setTitle(activity.getString(R.string.log_out_account, "@"+item.self.username));
|
||||
UiUtils.enablePopupMenuIcons(activity, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable image){
|
||||
avatar.setImageDrawable(image);
|
||||
if(image instanceof Animatable a)
|
||||
a.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
setImage(index, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
AccountSessionManager.getInstance().setLastActiveAccountID(item.getID());
|
||||
activity.finish();
|
||||
activity.startActivity(new Intent(activity, MainActivity.class));
|
||||
}
|
||||
}
|
||||
|
||||
private static class WrappedAccount{
|
||||
public final AccountSession session;
|
||||
public final ImageLoaderRequest req;
|
||||
|
||||
public WrappedAccount(AccountSession session){
|
||||
this.session=session;
|
||||
req=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? session.self.avatar : session.self.avatarStatic, V.dp(50), V.dp(50));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.joinmastodon.android.ui;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class ClickableSingleViewRecyclerAdapter extends SingleViewRecyclerAdapter{
|
||||
private final Runnable onClick;
|
||||
|
||||
public ClickableSingleViewRecyclerAdapter(View view, Runnable onClick){
|
||||
super(view);
|
||||
this.onClick=onClick;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new ClickableViewViewHolder(view);
|
||||
}
|
||||
|
||||
public class ClickableViewViewHolder extends ViewViewHolder implements UsableRecyclerView.Clickable{
|
||||
public ClickableViewViewHolder(@NonNull View itemView){
|
||||
super(itemView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
onClick.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ public class DividerItemDecoration extends RecyclerView.ItemDecoration{
|
||||
private Paint paint=new Paint();
|
||||
private int paddingStart, paddingEnd;
|
||||
private Predicate<RecyclerView.ViewHolder> drawDividerPredicate;
|
||||
private boolean drawBelowLastItem;
|
||||
|
||||
public static final Predicate<RecyclerView.ViewHolder> NOT_FIRST=vh->vh.getAbsoluteAdapterPosition()>0;
|
||||
|
||||
@@ -34,6 +35,10 @@ public class DividerItemDecoration extends RecyclerView.ItemDecoration{
|
||||
this.drawDividerPredicate=drawDividerPredicate;
|
||||
}
|
||||
|
||||
public void setDrawBelowLastItem(boolean drawBelowLastItem){
|
||||
this.drawBelowLastItem=drawBelowLastItem;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
boolean isRTL=parent.getLayoutDirection()==View.LAYOUT_DIRECTION_RTL;
|
||||
@@ -43,7 +48,7 @@ public class DividerItemDecoration extends RecyclerView.ItemDecoration{
|
||||
for(int i=0;i<parent.getChildCount();i++){
|
||||
View child=parent.getChildAt(i);
|
||||
int pos=parent.getChildAdapterPosition(child);
|
||||
if(pos<totalItems-1 && (drawDividerPredicate==null || drawDividerPredicate.test(parent.getChildViewHolder(child)))){
|
||||
if((drawBelowLastItem || pos<totalItems-1) && (drawDividerPredicate==null || drawDividerPredicate.test(parent.getChildViewHolder(child)))){
|
||||
float y=Math.round(child.getY()+child.getHeight());
|
||||
y-=(y-paint.getStrokeWidth()/2f)%1f; // Make sure the line aligns with the pixel grid
|
||||
paint.setAlpha(Math.round(255f*child.getAlpha()));
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.ui.drawables.SawtoothTearDrawable;
|
||||
|
||||
// Mind the gap!
|
||||
public class GapStatusDisplayItem extends StatusDisplayItem{
|
||||
public boolean loading;
|
||||
|
||||
public GapStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment){
|
||||
super(parentID, parentFragment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType(){
|
||||
return Type.GAP;
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<GapStatusDisplayItem>{
|
||||
public final ProgressBar progress;
|
||||
public final TextView text;
|
||||
|
||||
public Holder(Context context, ViewGroup parent){
|
||||
super(context, R.layout.display_item_gap, parent);
|
||||
progress=findViewById(R.id.progress);
|
||||
text=findViewById(R.id.text);
|
||||
itemView.setForeground(new SawtoothTearDrawable(context));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(GapStatusDisplayItem item){
|
||||
text.setVisibility(item.loading ? View.GONE : View.VISIBLE);
|
||||
progress.setVisibility(item.loading ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
item.parentFragment.onGapClick(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,7 @@ public abstract class StatusDisplayItem{
|
||||
case ACCOUNT_CARD -> new AccountCardStatusDisplayItem.Holder(activity, parent);
|
||||
case ACCOUNT -> new AccountStatusDisplayItem.Holder(activity, parent);
|
||||
case HASHTAG -> new HashtagStatusDisplayItem.Holder(activity, parent);
|
||||
case GAP -> new GapStatusDisplayItem.Holder(activity, parent);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -112,6 +113,8 @@ public abstract class StatusDisplayItem{
|
||||
}
|
||||
if(addFooter){
|
||||
items.add(new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID));
|
||||
if(status.hasGapAfter)
|
||||
items.add(new GapStatusDisplayItem(parentID, fragment));
|
||||
}
|
||||
int i=1;
|
||||
for(StatusDisplayItem item:items){
|
||||
@@ -142,7 +145,8 @@ public abstract class StatusDisplayItem{
|
||||
FOOTER,
|
||||
ACCOUNT_CARD,
|
||||
ACCOUNT,
|
||||
HASHTAG
|
||||
HASHTAG,
|
||||
GAP
|
||||
}
|
||||
|
||||
public static abstract class Holder<T extends StatusDisplayItem> extends BindableViewHolder<T> implements UsableRecyclerView.DisableableClickable{
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package org.joinmastodon.android.ui.drawables;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapShader;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Shader;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class SawtoothTearDrawable extends Drawable{
|
||||
private final Paint topPaint, bottomPaint;
|
||||
|
||||
private static final int TOP_SAWTOOTH_HEIGHT=5;
|
||||
private static final int BOTTOM_SAWTOOTH_HEIGHT=3;
|
||||
private static final int STROKE_WIDTH=2;
|
||||
private static final int SAWTOOTH_PERIOD=14;
|
||||
|
||||
public SawtoothTearDrawable(Context context){
|
||||
topPaint=makeShaderPaint(makeSawtoothTexture(context, TOP_SAWTOOTH_HEIGHT, SAWTOOTH_PERIOD, false, STROKE_WIDTH));
|
||||
bottomPaint=makeShaderPaint(makeSawtoothTexture(context, BOTTOM_SAWTOOTH_HEIGHT, SAWTOOTH_PERIOD, true, STROKE_WIDTH));
|
||||
Matrix matrix=new Matrix();
|
||||
//noinspection IntegerDivisionInFloatingPointContext
|
||||
matrix.setTranslate(V.dp(SAWTOOTH_PERIOD/2), 0);
|
||||
bottomPaint.getShader().setLocalMatrix(matrix);
|
||||
}
|
||||
|
||||
private Bitmap makeSawtoothTexture(Context context, int height, int period, boolean fillBottom, int strokeWidth){
|
||||
int actualStrokeWidth=V.dp(strokeWidth);
|
||||
int actualPeriod=V.dp(period);
|
||||
int actualHeight=V.dp(height);
|
||||
Bitmap bitmap=Bitmap.createBitmap(actualPeriod, actualHeight+actualStrokeWidth*2, Bitmap.Config.ARGB_8888);
|
||||
Canvas c=new Canvas(bitmap);
|
||||
Path path=new Path();
|
||||
//noinspection SuspiciousNameCombination
|
||||
path.moveTo(-actualPeriod/2f, actualStrokeWidth);
|
||||
path.lineTo(0, actualHeight+actualStrokeWidth);
|
||||
//noinspection SuspiciousNameCombination
|
||||
path.lineTo(actualPeriod/2f, actualStrokeWidth);
|
||||
path.lineTo(actualPeriod, actualHeight+actualStrokeWidth);
|
||||
//noinspection SuspiciousNameCombination
|
||||
path.lineTo(actualPeriod*1.5f, actualStrokeWidth);
|
||||
if(fillBottom){
|
||||
path.lineTo(actualPeriod*1.5f, actualHeight*20);
|
||||
path.lineTo(-actualPeriod/2f, actualHeight*20);
|
||||
}else{
|
||||
path.lineTo(actualPeriod*1.5f, -actualHeight);
|
||||
path.lineTo(-actualPeriod/2f, -actualHeight);
|
||||
}
|
||||
path.close();
|
||||
Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
paint.setColor(UiUtils.getThemeColor(context, R.attr.colorWindowBackground));
|
||||
c.drawPath(path, paint);
|
||||
paint.setColor(UiUtils.getThemeColor(context, R.attr.colorPollVoted));
|
||||
paint.setStrokeWidth(actualStrokeWidth);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
c.drawPath(path, paint);
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
private Paint makeShaderPaint(Bitmap bitmap){
|
||||
BitmapShader shader=new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.CLAMP);
|
||||
Paint paint=new Paint();
|
||||
paint.setShader(shader);
|
||||
return paint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas){
|
||||
int strokeWidth=V.dp(STROKE_WIDTH);
|
||||
Rect bounds=getBounds();
|
||||
canvas.save();
|
||||
canvas.translate(bounds.left, bounds.top);
|
||||
canvas.drawRect(0, 0, bounds.width(), V.dp(TOP_SAWTOOTH_HEIGHT)+strokeWidth*2, topPaint);
|
||||
int bottomHeight=V.dp(BOTTOM_SAWTOOTH_HEIGHT)+strokeWidth*2;
|
||||
canvas.translate(0, bounds.height()-bottomHeight);
|
||||
canvas.drawRect(0, 0, bounds.width(), bottomHeight, bottomPaint);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(@Nullable ColorFilter colorFilter){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity(){
|
||||
return PixelFormat.TRANSLUCENT;
|
||||
}
|
||||
}
|
||||
@@ -133,18 +133,18 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=29){
|
||||
DisplayCutout cutout=insets.getDisplayCutout();
|
||||
Insets tappable=insets.getTappableElementInsets();
|
||||
if(cutout!=null){
|
||||
// Make controls extend beneath the cutout, and replace insets to avoid cutout insets being filled with "navigation bar color"
|
||||
Insets tappable=insets.getTappableElementInsets();
|
||||
int leftInset=Math.max(0, cutout.getSafeInsetLeft()-tappable.left);
|
||||
int rightInset=Math.max(0, cutout.getSafeInsetRight()-tappable.right);
|
||||
insets=insets.replaceSystemWindowInsets(tappable.left, tappable.top, tappable.right, tappable.bottom);
|
||||
toolbarWrap.setPadding(leftInset, 0, rightInset, 0);
|
||||
videoControls.setPadding(leftInset, 0, rightInset, 0);
|
||||
}else{
|
||||
toolbarWrap.setPadding(0, 0, 0, 0);
|
||||
videoControls.setPadding(0, 0, 0, 0);
|
||||
}
|
||||
insets=insets.replaceSystemWindowInsets(tappable.left, tappable.top, tappable.right, tappable.bottom);
|
||||
}
|
||||
uiOverlay.dispatchApplyWindowInsets(insets);
|
||||
int bottomInset=insets.getSystemWindowInsetBottom();
|
||||
|
||||
@@ -36,6 +36,7 @@ public class DiscoverInfoBannerHelper{
|
||||
case TRENDING_HASHTAGS -> R.string.trending_hashtags_info_banner;
|
||||
case TRENDING_LINKS -> R.string.trending_links_info_banner;
|
||||
case LOCAL_TIMELINE -> R.string.local_timeline_info_banner;
|
||||
case FEDERATED_TIMELINE -> R.string.federated_timeline_info_banner;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -59,6 +60,7 @@ public class DiscoverInfoBannerHelper{
|
||||
TRENDING_HASHTAGS,
|
||||
TRENDING_LINKS,
|
||||
LOCAL_TIMELINE,
|
||||
FEDERATED_TIMELINE,
|
||||
// ACCOUNTS
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,12 +141,15 @@ public class UiUtils{
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
public static String abbreviateNumber(int n){
|
||||
if(n<1000)
|
||||
if(n<1000){
|
||||
return String.format("%,d", n);
|
||||
else if(n<1_000_000)
|
||||
return String.format("%,.1fK", n/1000f);
|
||||
else
|
||||
return String.format("%,.1fM", n/1_000_000f);
|
||||
}else if(n<1_000_000){
|
||||
float a=n/1000f;
|
||||
return a>99f ? String.format("%,dK", (int)Math.floor(a)) : String.format("%,.1fK", a);
|
||||
}else{
|
||||
float a=n/1_000_000f;
|
||||
return a>99f ? String.format("%,dM", (int)Math.floor(a)) : String.format("%,.1fM", n/1_000_000f);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -182,7 +185,7 @@ public class UiUtils{
|
||||
String name=cursor.getString(0);
|
||||
if(name!=null)
|
||||
return name;
|
||||
}
|
||||
}catch(Throwable ignore){}
|
||||
}
|
||||
return uri.getLastPathSegment();
|
||||
}
|
||||
@@ -340,13 +343,38 @@ public class UiUtils{
|
||||
}
|
||||
|
||||
public static void setRelationshipToActionButton(Relationship relationship, Button button){
|
||||
boolean secondaryStyle;
|
||||
if(relationship.blocking){
|
||||
button.setText(R.string.button_blocked);
|
||||
}else if(relationship.muting){
|
||||
button.setText(R.string.button_muted);
|
||||
secondaryStyle=true;
|
||||
}else if(relationship.blockedBy){
|
||||
button.setText(R.string.button_follow);
|
||||
secondaryStyle=false;
|
||||
}else if(relationship.requested){
|
||||
button.setText(R.string.button_follow_pending);
|
||||
secondaryStyle=true;
|
||||
}else if(!relationship.following){
|
||||
button.setText(relationship.followedBy ? R.string.follow_back : R.string.button_follow);
|
||||
secondaryStyle=false;
|
||||
}else{
|
||||
button.setText(relationship.following ? R.string.button_following : R.string.button_follow);
|
||||
button.setText(R.string.button_following);
|
||||
secondaryStyle=true;
|
||||
}
|
||||
|
||||
button.setEnabled(!relationship.blockedBy);
|
||||
int attr=secondaryStyle ? R.attr.secondaryButtonStyle : android.R.attr.buttonStyle;
|
||||
TypedArray ta=button.getContext().obtainStyledAttributes(new int[]{attr});
|
||||
int styleRes=ta.getResourceId(0, 0);
|
||||
ta.recycle();
|
||||
ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background});
|
||||
button.setBackground(ta.getDrawable(0));
|
||||
ta.recycle();
|
||||
ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.textColor});
|
||||
if(relationship.blocking)
|
||||
button.setTextColor(button.getResources().getColorStateList(R.color.error_600));
|
||||
else
|
||||
button.setTextColor(ta.getColorStateList(0));
|
||||
ta.recycle();
|
||||
}
|
||||
|
||||
public static void performAccountAction(Activity activity, Account account, String accountID, Relationship relationship, Button button, Consumer<Boolean> progressCallback, Consumer<Relationship> resultCallback){
|
||||
@@ -356,7 +384,7 @@ public class UiUtils{
|
||||
confirmToggleMuteUser(activity, accountID, account, true, resultCallback);
|
||||
}else{
|
||||
progressCallback.accept(true);
|
||||
new SetAccountFollowed(account.id, !relationship.following, true)
|
||||
new SetAccountFollowed(account.id, !relationship.following && !relationship.requested, true)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Relationship result){
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
public class AutoOrientationLinearLayout extends LinearLayout{
|
||||
public AutoOrientationLinearLayout(Context context){
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public AutoOrientationLinearLayout(Context context, AttributeSet attrs){
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public AutoOrientationLinearLayout(Context context, AttributeSet attrs, int defStyle){
|
||||
super(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
|
||||
int hPadding=getPaddingLeft()+getPaddingRight();
|
||||
int childrenTotalWidth=0;
|
||||
for(int i=0;i<getChildCount();i++){
|
||||
View child=getChildAt(i);
|
||||
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
|
||||
childrenTotalWidth+=child.getMeasuredWidth();
|
||||
}
|
||||
if(childrenTotalWidth>MeasureSpec.getSize(widthMeasureSpec)-hPadding){
|
||||
setOrientation(VERTICAL);
|
||||
}else{
|
||||
setOrientation(HORIZONTAL);
|
||||
}
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.joinmastodon.android.utils;
|
||||
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class StatusFilterPredicate implements Predicate<Status>{
|
||||
private final List<Filter> filters;
|
||||
|
||||
public StatusFilterPredicate(List<Filter> filters){
|
||||
this.filters=filters;
|
||||
}
|
||||
|
||||
public StatusFilterPredicate(String accountID, Filter.FilterContext context){
|
||||
filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(context)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean test(Status status){
|
||||
CharSequence content=status.getContentStatus().content;
|
||||
for(Filter filter:filters){
|
||||
if(filter.matches(content))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
9
mastodon/src/main/res/drawable/bg_bottom_sheet.xml
Normal file
9
mastodon/src/main/res/drawable/bg_bottom_sheet.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape>
|
||||
<solid android:color="?colorWindowBackground"/>
|
||||
<corners android:topLeftRadius="12dp" android:topRightRadius="12dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
12
mastodon/src/main/res/drawable/bg_bottom_sheet_handle.xml
Normal file
12
mastodon/src/main/res/drawable/bg_bottom_sheet_handle.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:gravity="center" android:width="36dp" android:height="4dp">
|
||||
<shape>
|
||||
<solid android:color="?android:textColorSecondary"/>
|
||||
<corners android:radius="2dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
<item android:height="20dp">
|
||||
<color android:color="#00000000"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
10
mastodon/src/main/res/drawable/bg_button_new_posts.xml
Normal file
10
mastodon/src/main/res/drawable/bg_button_new_posts.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="@color/highlight_over_dark">
|
||||
<item>
|
||||
<shape>
|
||||
<solid android:color="?android:colorAccent"/>
|
||||
<corners android:radius="16dp"/>
|
||||
<padding android:left="16dp" android:right="16dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#E6000000"/>
|
||||
<corners android:radius="5dp"/>
|
||||
</shape>
|
||||
7
mastodon/src/main/res/drawable/bg_timeline_gap.xml
Normal file
7
mastodon/src/main/res/drawable/bg_timeline_gap.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<color android:color="?colorBackgroundLightest"/>
|
||||
</item>
|
||||
<item android:drawable="?android:selectableItemBackground"/>
|
||||
</layer-list>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||
<path android:pathData="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2zm0 5c-0.38 0-0.694 0.282-0.743 0.648L11.25 7.75v3.5h-3.5C7.336 11.25 7 11.586 7 12c0 0.38 0.282 0.694 0.648 0.743L7.75 12.75h3.5v3.5c0 0.414 0.336 0.75 0.75 0.75 0.38 0 0.694-0.282 0.743-0.648l0.007-0.102v-3.5h3.5c0.414 0 0.75-0.336 0.75-0.75 0-0.38-0.282-0.694-0.648-0.743L16.25 11.25h-3.5v-3.5C12.75 7.336 12.414 7 12 7z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="16dp" android:height="16dp" android:viewportWidth="16" android:viewportHeight="16">
|
||||
<path android:pathData="M8 14c-0.414 0-0.75-0.336-0.75-0.75V4.463L4.309 7.75c-0.276 0.31-0.75 0.335-1.06 0.06-0.308-0.276-0.334-0.75-0.058-1.06L7.441 2C7.583 1.842 7.787 1.75 8 1.75c0.213 0 0.417 0.09 0.559 0.25l4.25 4.75c0.276 0.309 0.25 0.783-0.059 1.059-0.309 0.276-0.783 0.25-1.059-0.059L8.75 4.463v8.787C8.75 13.664 8.414 14 8 14z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||
<path android:pathData="M8.5 16.586l-3.793-3.793c-0.39-0.39-1.024-0.39-1.414 0-0.39 0.39-0.39 1.024 0 1.414l4.5 4.5c0.39 0.39 1.024 0.39 1.414 0l11-11c0.39-0.39 0.39-1.024 0-1.414-0.39-0.39-1.024-0.39-1.414 0L8.5 16.586z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="16dp" android:height="16dp" android:viewportWidth="16" android:viewportHeight="16">
|
||||
<path android:pathData="M4.146 6.354c0.196 0.195 0.512 0.195 0.708 0L8 3.207l3.146 3.147c0.196 0.195 0.512 0.195 0.708 0 0.195-0.196 0.195-0.512 0-0.708l-3.5-3.5c-0.196-0.195-0.512-0.195-0.707 0l-3.5 3.5c-0.196 0.196-0.196 0.512 0 0.708zm0 3.292c0.196-0.195 0.512-0.195 0.708 0L8 12.793l3.146-3.146c0.196-0.196 0.512-0.196 0.708 0 0.195 0.195 0.195 0.511 0 0.707l-3.5 3.5c-0.196 0.195-0.512 0.195-0.707 0l-3.5-3.5c-0.196-0.196-0.196-0.512 0-0.707z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="20dp" android:height="20dp" android:viewportWidth="20" android:viewportHeight="20">
|
||||
<path android:pathData="M10 2c1.657 0 3 1.343 3 3v1h1c1.105 0 2 0.895 2 2v7c0 1.105-0.895 2-2 2H6c-1.105 0-2-0.895-2-2V8c0-1.105 0.895-2 2-2h1V5c0-1.657 1.343-3 3-3zm0 8.5c-0.552 0-1 0.448-1 1s0.448 1 1 1 1-0.448 1-1-0.448-1-1-1zM10 4C9.448 4 9 4.448 9 5v1h2V5c0-0.552-0.448-1-1-1z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||
<path android:pathData="M12 7.75c-0.966 0-1.75-0.784-1.75-1.75S11.034 4.25 12 4.25 13.75 5.034 13.75 6 12.966 7.75 12 7.75zm0 6c-0.966 0-1.75-0.784-1.75-1.75s0.784-1.75 1.75-1.75 1.75 0.784 1.75 1.75-0.784 1.75-1.75 1.75zM10.25 18c0 0.966 0.784 1.75 1.75 1.75s1.75-0.784 1.75-1.75-0.784-1.75-1.75-1.75-1.75 0.784-1.75 1.75z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="25dp" android:height="24dp" android:viewportWidth="25" android:viewportHeight="24">
|
||||
<path android:pathData="M11.416 17.5c0-1.288 0.375-2.49 1.022-3.5h-7.77c-1.242 0-2.249 1.007-2.249 2.25v0.919c0 0.572 0.179 1.13 0.51 1.596C4.473 20.929 6.996 22 10.417 22c0.931 0 1.796-0.08 2.592-0.238-0.992-1.142-1.592-2.632-1.592-4.263zm-1-15.495c2.761 0 5 2.239 5 5s-2.239 5-5 5c-2.762 0-5-2.239-5-5s2.238-5 5-5zm13 15.495c0 3.038-2.463 5.5-5.5 5.5-3.038 0-5.5-2.462-5.5-5.5 0-3.037 2.462-5.5 5.5-5.5 3.037 0 5.5 2.463 5.5 5.5zm-4.647-2.853c-0.195-0.196-0.511-0.196-0.707 0-0.195 0.195-0.195 0.512 0 0.707L19.71 17h-4.293c-0.276 0-0.5 0.224-0.5 0.5s0.224 0.5 0.5 0.5h4.293l-1.647 1.647c-0.195 0.195-0.195 0.512 0 0.707 0.196 0.195 0.512 0.195 0.707 0l2.5-2.5c0.054-0.053 0.092-0.116 0.117-0.182 0.018-0.05 0.029-0.105 0.03-0.163V17.5c0-0.077-0.018-0.15-0.049-0.215-0.015-0.032-0.034-0.063-0.057-0.092-0.013-0.018-0.028-0.035-0.045-0.05l-2.496-2.496z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
</vector>
|
||||
24
mastodon/src/main/res/layout/display_item_gap.xml
Normal file
24
mastodon/src/main/res/layout/display_item_gap.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="75dp"
|
||||
android:background="@drawable/bg_timeline_gap">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:gravity="center_horizontal"
|
||||
android:textAppearance="@style/m3_body_large"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:text="@string/load_missing_posts"/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"/>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -15,6 +15,13 @@
|
||||
android:paddingBottom="16dp"
|
||||
android:background="?android:statusBarColor">
|
||||
|
||||
<!-- https://github.com/mastodon/mastodon-android/issues/95 -->
|
||||
<View
|
||||
android:layout_width="1px"
|
||||
android:layout_height="1px"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"/>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="23dp"
|
||||
android:paddingBottom="15dp"
|
||||
android:clipToPadding="false">
|
||||
|
||||
<org.joinmastodon.android.ui.views.CoverImageView
|
||||
@@ -33,6 +33,25 @@
|
||||
android:contentDescription="@string/profile_header"
|
||||
android:scaleType="centerCrop"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/follows_you"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="28dp"
|
||||
android:layout_alignEnd="@id/cover"
|
||||
android:layout_alignBottom="@id/cover"
|
||||
android:layout_margin="16dp"
|
||||
android:paddingRight="8dp"
|
||||
android:paddingLeft="8dp"
|
||||
android:textColor="@color/gray_50t"
|
||||
android:textAllCaps="true"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:textSize="14dp"
|
||||
android:gravity="center"
|
||||
android:background="@drawable/bg_profile_follows_you"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
android:text="@string/follows_you"/>
|
||||
|
||||
<View
|
||||
android:id="@+id/avatar_border"
|
||||
android:layout_width="102dp"
|
||||
@@ -56,80 +75,95 @@
|
||||
tools:src="#0f0" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/following_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="56dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:id="@+id/profile_counters"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/cover"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:padding="4dp"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_horizontal">
|
||||
<TextView
|
||||
android:id="@+id/following_count"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/m3_title_large"
|
||||
tools:text="123"/>
|
||||
<TextView
|
||||
android:id="@+id/following_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/m3_title_small"
|
||||
tools:text="following"/>
|
||||
</LinearLayout>
|
||||
android:layout_toEndOf="@id/avatar_border"
|
||||
android:gravity="end">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/followers_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="56dp"
|
||||
android:layout_toStartOf="@id/following_btn"
|
||||
android:layout_below="@id/cover"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:padding="4dp"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_horizontal">
|
||||
<TextView
|
||||
android:id="@+id/followers_count"
|
||||
<LinearLayout
|
||||
android:id="@+id/posts_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/m3_title_large"
|
||||
tools:text="123"/>
|
||||
<TextView
|
||||
android:id="@+id/followers_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/m3_title_small"
|
||||
tools:text="following"/>
|
||||
</LinearLayout>
|
||||
android:layout_height="56dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:padding="4dp">
|
||||
<TextView
|
||||
android:id="@+id/posts_count"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/m3_title_large"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="end"
|
||||
tools:text="123" />
|
||||
<TextView
|
||||
android:id="@+id/posts_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/m3_title_small"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="middle"
|
||||
tools:text="posts" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/posts_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="56dp"
|
||||
android:layout_below="@id/cover"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_toStartOf="@id/followers_btn"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:padding="4dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/posts_count"
|
||||
<LinearLayout
|
||||
android:id="@+id/following_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/m3_title_large"
|
||||
tools:text="123" />
|
||||
android:layout_height="56dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:padding="4dp"
|
||||
android:orientation="vertical"
|
||||
android:background="?android:selectableItemBackgroundBorderless"
|
||||
android:gravity="center_horizontal">
|
||||
<TextView
|
||||
android:id="@+id/following_count"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/m3_title_large"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="end"
|
||||
tools:text="123"/>
|
||||
<TextView
|
||||
android:id="@+id/following_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/m3_title_small"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="middle"
|
||||
tools:text="following"/>
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/posts_label"
|
||||
<LinearLayout
|
||||
android:id="@+id/followers_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/m3_title_small"
|
||||
tools:text="following" />
|
||||
android:layout_height="56dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:padding="4dp"
|
||||
android:orientation="vertical"
|
||||
android:background="?android:selectableItemBackgroundBorderless"
|
||||
android:gravity="center_horizontal">
|
||||
<TextView
|
||||
android:id="@+id/followers_count"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/m3_title_large"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="end"
|
||||
tools:text="123"/>
|
||||
<TextView
|
||||
android:id="@+id/followers_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/m3_title_small"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="middle"
|
||||
tools:text="followers"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
@@ -137,7 +171,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_below="@id/following_btn"
|
||||
android:layout_below="@id/profile_counters"
|
||||
android:padding="16dp"
|
||||
android:clipToPadding="false">
|
||||
<org.joinmastodon.android.ui.views.ProgressBarButton
|
||||
|
||||
62
mastodon/src/main/res/layout/item_account_list.xml
Normal file
62
mastodon/src/main/res/layout/item_account_list.xml
Normal file
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="62dp"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:clipToPadding="false">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/avatar"
|
||||
android:layout_width="46dp"
|
||||
android:layout_height="46dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:importantForAccessibility="no"
|
||||
tools:src="#0f0"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginStart="8dp"
|
||||
tools:text="Follow"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="24dp"
|
||||
android:layout_toEndOf="@id/avatar"
|
||||
android:layout_toStartOf="@id/button"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical"
|
||||
android:textAppearance="@style/m3_title_medium"
|
||||
tools:text="User"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/username"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="20dp"
|
||||
android:layout_below="@id/name"
|
||||
android:layout_toEndOf="@id/avatar"
|
||||
android:layout_toStartOf="@id/button"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical"
|
||||
android:textAppearance="@style/m3_title_small"
|
||||
tools:text="\@user@server"/>
|
||||
|
||||
<View
|
||||
android:id="@+id/menu_anchor"
|
||||
android:layout_width="1px"
|
||||
android:layout_height="1px"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_marginLeft="-16dp"/>
|
||||
|
||||
</RelativeLayout>
|
||||
44
mastodon/src/main/res/layout/item_account_switcher.xml
Normal file
44
mastodon/src/main/res/layout/item_account_switcher.xml
Normal file
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:paddingLeft="20dp"
|
||||
android:paddingRight="20dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/avatar"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:importantForAccessibility="no"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="24dp"
|
||||
android:textSize="16dp"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="end"/>
|
||||
|
||||
<View
|
||||
android:id="@+id/current"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:background="@drawable/ic_fluent_checkmark_24_filled"
|
||||
android:backgroundTint="?android:textColorSecondary"
|
||||
android:contentDescription="@string/current_account"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/more"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/ic_fluent_more_vertical_24_regular"
|
||||
android:tint="?android:textColorSecondary"
|
||||
android:contentDescription="@string/more_options"
|
||||
android:background="?android:selectableItemBackgroundBorderless"/>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -1,16 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<org.joinmastodon.android.ui.views.AutoOrientationLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="32dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layoutDirection="locale"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:gravity="center_vertical"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textSize="20dp"
|
||||
@@ -19,12 +20,10 @@
|
||||
android:ellipsize="end"
|
||||
android:text="@string/notify_me_when"/>
|
||||
|
||||
<!-- TODO make this LL vertical when the button doesn't fit -->
|
||||
<Button
|
||||
android:id="@+id/button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_height="32dp"
|
||||
android:background="@drawable/bg_inline_button"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textSize="20dp"
|
||||
@@ -37,4 +36,4 @@
|
||||
android:ellipsize="middle"
|
||||
tools:text="@string/notify_followed"/>
|
||||
|
||||
</LinearLayout>
|
||||
</org.joinmastodon.android.ui.views.AutoOrientationLinearLayout>
|
||||
@@ -12,11 +12,11 @@
|
||||
android:id="@+id/tabbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="52dp"
|
||||
android:paddingLeft="20dp"
|
||||
android:paddingRight="20dp">
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp">
|
||||
<ImageView
|
||||
android:id="@+id/tab_home"
|
||||
android:layout_width="52dp"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="52dp"
|
||||
android:scaleType="center"
|
||||
android:contentDescription="@string/home_timeline"
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/tab_search"
|
||||
android:layout_width="52dp"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="52dp"
|
||||
android:scaleType="center"
|
||||
android:contentDescription="@string/search_hint"
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/tab_notifications"
|
||||
android:layout_width="52dp"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="52dp"
|
||||
android:scaleType="center"
|
||||
android:contentDescription="@string/notifications"
|
||||
@@ -61,7 +61,7 @@
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/tab_profile"
|
||||
android:layout_width="52dp"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="52dp"
|
||||
android:contentDescription="@string/my_profile"
|
||||
android:foreground="@drawable/bg_tab_profile"
|
||||
@@ -73,6 +73,13 @@
|
||||
android:layout_gravity="center"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@null"/>
|
||||
<View
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:layout_marginEnd="-4dp"
|
||||
android:backgroundTint="?android:colorPrimary"
|
||||
android:background="@drawable/ic_fluent_chevron_up_down_16_regular"/>
|
||||
</FrameLayout>
|
||||
</org.joinmastodon.android.ui.views.TabBar>
|
||||
|
||||
|
||||
4
mastodon/src/main/res/menu/account_switcher.xml
Normal file
4
mastodon/src/main/res/menu/account_switcher.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@+id/log_out" android:title="@string/log_out_account" android:icon="@drawable/ic_fluent_person_arrow_right_24_filled"/>
|
||||
</menu>
|
||||
@@ -3,6 +3,9 @@
|
||||
<item android:id="@+id/vis_public"
|
||||
android:icon="@drawable/ic_fluent_earth_24_filled"
|
||||
android:title="@string/visibility_public"/>
|
||||
<item android:id="@+id/vis_unlisted"
|
||||
android:icon="@drawable/ic_fluent_people_community_24_regular"
|
||||
android:title="@string/visibility_unlisted"/>
|
||||
<item android:id="@+id/vis_followers"
|
||||
android:icon="@drawable/ic_fluent_people_checkmark_24_regular"
|
||||
android:title="@string/visibility_followers_only"/>
|
||||
|
||||
@@ -213,6 +213,7 @@
|
||||
<string name="alt_text_subtitle">Alternativtext erscheint für blinde Menschen. Versuche, nur so viele Details einzubeziehen, um den Kontext zu verstehen.</string>
|
||||
<string name="alt_text_hint">z.B. Eine Giraffe auf einem Dreirad während sie eine Banane isst</string>
|
||||
<string name="visibility_public">Öffentlich</string>
|
||||
<string name="visibility_unlisted">Nicht gelistet</string>
|
||||
<string name="visibility_followers_only">Nur Folgende</string>
|
||||
<string name="visibility_private">Nur Leute, die ich erwähne</string>
|
||||
<string name="search_all">Alle</string>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<item name="discover_news" type="id"/>
|
||||
<item name="discover_users" type="id"/>
|
||||
<item name="discover_local_timeline" type="id"/>
|
||||
<item name="discover_federated_timeline" type="id"/>
|
||||
|
||||
<item name="notifications_all" type="id"/>
|
||||
<item name="notifications_mentions" type="id"/>
|
||||
|
||||
@@ -11,14 +11,14 @@
|
||||
<string name="ok">OK</string>
|
||||
<string name="preparing_auth">Preparing for authentication…</string>
|
||||
<string name="finishing_auth">Finishing authentication…</string>
|
||||
<string name="user_boosted">%s boosted</string>
|
||||
<string name="user_boosted">%s reblogged</string>
|
||||
<string name="in_reply_to">In reply to %s</string>
|
||||
<string name="notifications">Notifications</string>
|
||||
|
||||
<string name="user_followed_you">followed you</string>
|
||||
<string name="user_sent_follow_request">sent you a follow request</string>
|
||||
<string name="user_favorited">favorited your toot</string>
|
||||
<string name="notification_boosted">boosted your toot</string>
|
||||
<string name="user_favorited">favorited your post</string>
|
||||
<string name="notification_boosted">reblogged your post</string>
|
||||
<string name="poll_ended">poll ended</string>
|
||||
|
||||
<string name="time_seconds">%ds</string>
|
||||
@@ -218,6 +218,7 @@
|
||||
<string name="alt_text_subtitle">Alt text describes your photos for people with low or no vision. Try to only include enough detail to understand the context.</string>
|
||||
<string name="alt_text_hint">e.g. A dog looking around suspiciously with narrowed eyes at the camera.</string>
|
||||
<string name="visibility_public">Public</string>
|
||||
<string name="visibility_unlisted">Unlisted</string>
|
||||
<string name="visibility_followers_only">Followers only</string>
|
||||
<string name="visibility_private">Only people I mention</string>
|
||||
<string name="search_all">All</string>
|
||||
@@ -227,7 +228,7 @@
|
||||
<string name="skip">Skip</string>
|
||||
<string name="notification_type_follow">New followers</string>
|
||||
<string name="notification_type_favorite">Favorites</string>
|
||||
<string name="notification_type_reblog">Boosts</string>
|
||||
<string name="notification_type_reblog">Reblogs</string>
|
||||
<string name="notification_type_mention">Mentions</string>
|
||||
<string name="notification_type_poll">Polls</string>
|
||||
<string name="choose_account">Choose account</string>
|
||||
@@ -290,8 +291,8 @@
|
||||
<string name="unfollowed_user">Unfollowed %s</string>
|
||||
<string name="followed_user">You\'re now following %s</string>
|
||||
<string name="open_in_browser">Open in browser</string>
|
||||
<string name="hide_boosts_from_user">Hide boosts from %s</string>
|
||||
<string name="show_boosts_from_user">Show boosts from %s</string>
|
||||
<string name="hide_boosts_from_user">Hide reblogs from %s</string>
|
||||
<string name="show_boosts_from_user">Show reblogs from %s</string>
|
||||
<string name="signup_reason">why do you want to join?</string>
|
||||
<string name="signup_reason_note">This will help us review your application.</string>
|
||||
<string name="clear">Clear</string>
|
||||
@@ -307,9 +308,37 @@
|
||||
<string name="downloading">Downloading…</string>
|
||||
<string name="no_app_to_handle_action">There\'s no app to handle this action</string>
|
||||
<string name="local_timeline">Community</string>
|
||||
<string name="federated_timeline">Federation</string>
|
||||
<string name="trending_posts_info_banner">These are the posts gaining traction in your corner of Mastodon.</string>
|
||||
<string name="trending_hashtags_info_banner">These are the hashtags gaining traction in your corner of Mastodon.</string>
|
||||
<string name="trending_links_info_banner">These are the news stories being shared the most in your corner of Mastodon.</string>
|
||||
<string name="local_timeline_info_banner">These are the most recent posts by the people who use the same Mastodon server as you.</string>
|
||||
<string name="federated_timeline_info_banner">These are the most recent posts by the people in your federation.</string>
|
||||
<string name="dismiss">Dismiss</string>
|
||||
<string name="see_new_posts">See new posts</string>
|
||||
<string name="load_missing_posts">Load missing posts</string>
|
||||
<string name="follow_back">Follow Back</string>
|
||||
<string name="button_follow_pending">Pending</string>
|
||||
<string name="follows_you">Follows you</string>
|
||||
<string name="manually_approves_followers">Manually approves followers</string>
|
||||
<string name="current_account">Current account</string>
|
||||
<string name="log_out_account">Log Out %s</string>
|
||||
|
||||
<!-- translators: %,d is a valid placeholder, it formats the number with locale-dependent grouping separators -->
|
||||
<plurals name="x_followers">
|
||||
<item quantity="one">%,d follower</item>
|
||||
<item quantity="other">%,d followers</item>
|
||||
</plurals>
|
||||
<plurals name="x_following">
|
||||
<item quantity="one">%,d following</item>
|
||||
<item quantity="other">%,d following</item>
|
||||
</plurals>
|
||||
<plurals name="x_favorites">
|
||||
<item quantity="one">%,d favorite</item>
|
||||
<item quantity="other">%,d favorites</item>
|
||||
</plurals>
|
||||
<plurals name="x_reblogs">
|
||||
<item quantity="one">%,d reblog</item>
|
||||
<item quantity="other">%,d reblogs</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
@@ -66,7 +66,7 @@
|
||||
<item name="colorButtonText">@color/gray_800</item>
|
||||
<item name="colorSecondary">#E9EDF2</item>
|
||||
<item name="colorBackgroundLight">@color/gray_700</item>
|
||||
<item name="colorBackgroundLightest">@color/gray_700</item>
|
||||
<item name="colorBackgroundLightest">@color/gray_900</item>
|
||||
<item name="colorDarkIcon">@color/gray_25</item>
|
||||
<item name="colorWindowBackground">@color/gray_800</item>
|
||||
<item name="android:statusBarColor">@color/gray_800</item>
|
||||
|
||||
Reference in New Issue
Block a user