Compare commits

...

72 Commits

Author SHA1 Message Date
sk
aa96ec54a3 Merge branch 'feature/delete-redraft' into fork 2022-05-26 22:09:39 +02:00
sk
e8b43c7179 preserve visibility when re-drafting 2022-05-26 22:09:02 +02:00
sk
b51b4a10ee Merge branch 'feature/delete-redraft' into fork 2022-05-26 21:44:07 +02:00
sk
f2b9ede27c Add proguard rules for parceler
according to http://parceler.org/
fixes issue where parceler can't find parcelable class
2022-05-26 21:43:39 +02:00
sk
a8c7d891f1 Merge branch 'feature/delete-redraft' into fork 2022-05-26 19:45:52 +02:00
sk
195c4d7b6d remove unused imports 2022-05-26 19:45:10 +02:00
sk
d280dc31e8 bump version 2022-05-26 19:35:36 +02:00
sk
eb0925c524 Merge branch 'feature/delete-redraft' into fork 2022-05-26 19:30:52 +02:00
sk
968de3664d fix german strings 2022-05-26 19:30:23 +02:00
sk
12f7336392 Merge branch 'feature/delete-redraft' into fork 2022-05-26 19:28:09 +02:00
sk
3a4d13b1c6 implement deleting and re-drafting 2022-05-26 19:19:42 +02:00
sk
273c841d9a Merge branch 'master' into feature/delete-redraft 2022-05-26 15:35:12 +02:00
sk
0186b7f8da Merge remote-tracking branch 'origin/fork' into fork 2022-05-22 02:14:06 +02:00
sk
d33654c793 bump version 2022-05-22 02:08:01 +02:00
Samuel Kaiser
86d2312615 Update README.md 2022-05-22 02:07:07 +02:00
sk
d1083c331b Merge branch 'feature/bookmarks' into fork 2022-05-22 02:04:08 +02:00
sk
ed7242217a add missing icon 2022-05-22 02:03:54 +02:00
sk
8fddaa8c82 implement bookmarks list!! 2022-05-22 02:03:29 +02:00
sk
00affe6e3e update readme 2022-05-21 23:38:56 +02:00
sk
f21b647ee0 bump version 2022-05-21 23:35:10 +02:00
sk
2a628a3791 Merge branch 'feature/back-returns-home' into fork 2022-05-21 23:34:18 +02:00
sk
ecd568503d make back button return home before exiting 2022-05-21 23:33:53 +02:00
sk
f9d0632a85 add missing files 2022-05-21 19:42:29 +02:00
sk
11905513b7 add missing icons 2022-05-21 19:40:49 +02:00
sk
9c89abf1c4 implement bookmark button 2022-05-21 19:27:44 +02:00
sk
4d950e43ac update readme 2022-05-21 18:59:13 +02:00
sk
99405f307d bump version 2022-05-21 18:49:59 +02:00
sk
f1bfe05263 Merge branch 'feature/always-preserve-cw' into fork 2022-05-21 18:49:07 +02:00
sk
0f223159c0 always preserve cw when replying 2022-05-21 18:48:48 +02:00
sk
ad9518e87c re-add deleted strings 2022-05-21 18:08:14 +02:00
sk
1c16cfb09e bump version 2022-05-21 18:05:01 +02:00
sk
d4a4b10017 Merge branch 'feature/compose-image-description-full-image' into fork 2022-05-21 18:03:29 +02:00
sk
74ae5bd04e change app name 2022-05-21 18:03:15 +02:00
sk
9638cf079f set image view height to wrap_content 2022-05-21 17:48:53 +02:00
sk
a6d161c1b4 minor code style change
for grishka's code style must prevail
2022-05-21 17:42:40 +02:00
sk
1136e40eb4 obey image max width 2022-05-21 17:39:28 +02:00
sk
98de3a2984 don't crop image when composing alt text 2022-05-21 17:27:31 +02:00
Grishka
080a320e12 Make the app name non-translatable 2022-05-17 18:47:11 +03:00
sk
b08415ca8f bump version 2022-05-15 20:35:10 +02:00
sk
3639c69d36 Merge branch 'master' into fork 2022-05-15 20:34:14 +02:00
Grishka
37cefcaf6d Fix #164 2022-05-15 21:13:36 +03:00
Grishka
558adc6936 Add compose shortcut
closes #131
2022-05-15 19:14:24 +03:00
sk
31e3a8592f bump version 2022-05-14 13:49:49 +02:00
sk
39655d5278 Merge branch 'master' into fork 2022-05-14 13:48:03 +02:00
Grishka
68d0862008 Close #122 2022-05-13 20:54:22 +03:00
Grishka
c9e13eefa5 Close #146 2022-05-13 20:49:35 +03:00
Grishka
349fbce5af Fix #128 2022-05-13 20:42:54 +03:00
Grishka
95c66654aa Fix #149 2022-05-13 19:20:40 +03:00
Grishka
a8407571a4 Fix #151 2022-05-13 19:18:29 +03:00
Grishka
75538deb9b Fix #156 2022-05-13 19:10:27 +03:00
Grishka
601eec4607 Fix #157 2022-05-13 19:01:29 +03:00
Grishka
9b87d0bece Fix #153 2022-05-13 18:14:52 +03:00
Grishka
cb25632691 Delete statuses from cache and fix auto-refresh when posting 2022-05-13 17:57:41 +03:00
Grishka
63957250c5 Fix #141 + crash fixes 2022-05-13 17:51:28 +03:00
sk
d844a77e65 add ui items for redraft 2022-05-11 17:25:00 +02:00
sk
bde2e398a8 bump version and change app name 2022-05-06 22:10:45 +02:00
sk
8d443b2051 Merge branch 'feature/pin-posts' into fork 2022-05-06 21:50:08 +02:00
sk
33d4b678ed update posts' pinned states 2022-05-06 21:49:33 +02:00
sk
3becad1468 fix created posts being added to pinned 2022-05-06 21:18:48 +02:00
sk
fad3ba3eae bump version 2022-05-06 19:49:36 +02:00
sk
cb16f95878 Merge branch 'feature/pin-posts' into fork 2022-05-06 19:45:33 +02:00
sk
4e833490ff fix about section not being displayed 2022-05-06 19:45:09 +02:00
Samuel Kaiser
04a973f7b0 Update README.md 2022-05-06 19:28:43 +02:00
sk
0318169b74 Merge branch 'feature/pin-posts' into fork 2022-05-06 19:24:58 +02:00
sk
972fb1e241 translate "pinned" strings to german 2022-05-06 19:21:39 +02:00
sk
9beb04b01d implement pinning and unpinning posts 2022-05-06 19:07:51 +02:00
sk
a3bea6ad24 add profile tab for pinned toots 2022-05-06 18:09:00 +02:00
sk
7996e4ee4a change client name, versioning 2022-05-06 15:39:57 +02:00
sk
69c4bf4213 Merge branch 'feature/display-alt-text' into fork 2022-05-06 01:00:34 +02:00
sk
7cd5ca77f5 Merge remote-tracking branch 'origin/feature/display-alt-text' into feature/display-alt-text 2022-05-06 01:00:01 +02:00
sk
7e736d3cd3 clean up code 2022-05-06 00:59:43 +02:00
Samuel Kaiser
f0cef2103f Merge branch 'mastodon:master' into feature/display-alt-text 2022-05-06 00:29:05 +02:00
53 changed files with 872 additions and 188 deletions

View File

@@ -1,17 +1,22 @@
# Forked Mastodon for Android
[![Crowdin](https://badges.crowdin.net/mastodon-for-android/localized.svg)](https://crowdin.com/project/mastodon-for-android)
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/).
Learn more about the official 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)
* [Add "Federation" tab and change Discover tab order](https://github.com/sk22/mastodon-android-fork/tree/feature/add-federated-timeline) ([Fixes issue](https://github.com/mastodon/mastodon-android/issues/8))
* [Add image description button and viewer](https://github.com/sk22/mastodon-android-fork/tree/feature/display-alt-text) ([Pull request](https://github.com/mastodon/mastodon-android/pull/129))
* [Implement pinning posts and displaying pinned posts](https://github.com/sk22/mastodon-android-fork/tree/feature/pin-posts) ([Pull request](https://github.com/mastodon/mastodon-android/pull/140))
* [Display full image when adding image description](https://github.com/sk22/mastodon-android-fork/tree/feature/compose-image-description-full-image) ([Pull request](https://github.com/mastodon/mastodon-android/pull/182))
* [Always preserve content warnings when replying](https://github.com/sk22/mastodon-android-fork/tree/feature/always-preserve-cw) ([Fixes issue](https://github.com/mastodon/mastodon-android/issues/113))
* [Make back button return to the home tab before exiting the app](https://github.com/sk22/mastodon-android-fork/tree/feature/back-returns-home) ([Fixes issue](https://github.com/mastodon/mastodon-android/issues/118))
* [Implement a bookmark button and list](https://github.com/sk22/mastodon-android-fork/tree/feature/bookmarks) ([Fixes issue](https://github.com/mastodon/mastodon-android/issues/22))
* [Implement deleting and re-drafting](https://github.com/sk22/mastodon-android-fork/tree/feature/delete-redraft) ([Fixes issue](https://github.com/mastodon/mastodon-android/issues/21))
## Building
@@ -23,4 +28,4 @@ As this app is using Java 17 features, you need JDK 17 or newer to build it. Oth
## License
This project is released under the [GPL-3 License](./LICENSE).
This project is released under the [GPL-3 License](./LICENSE).

View File

@@ -9,8 +9,8 @@ android {
applicationId "org.joinmastodon.android.sk"
minSdk 23
targetSdk 31
versionCode 5
versionName '1.1.1-dev+fork.1'
versionCode 15
versionName '1.1.1+fork.15'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -46,4 +46,9 @@
-keep class org.joinmastodon.android.AppCenterWrapper { *; }
-keepattributes LineNumberTable
-keepattributes LineNumberTable
# Parceler library
-keep interface org.parceler.Parcel
-keep @org.parceler.Parcel class * { *; }
-keep class **$$Parcelable { *; }

View File

@@ -9,6 +9,7 @@ import android.util.Log;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.SplashFragment;
@@ -56,6 +57,8 @@ public class MainActivity extends FragmentStackActivity{
if(intent.getBooleanExtra("fromNotification", false) && intent.hasExtra("notification")){
Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
showFragmentForNotification(notification, session.getID());
}else if(intent.getBooleanExtra("compose", false)){
showCompose();
}
}
}
@@ -91,6 +94,8 @@ public class MainActivity extends FragmentStackActivity{
fragment.setArguments(args);
showFragmentClearingBackStack(fragment);
}
}else if(intent.getBooleanExtra("compose", false)){
showCompose();
}
}
@@ -115,4 +120,15 @@ public class MainActivity extends FragmentStackActivity{
fragment.setArguments(args);
showFragment(fragment);
}
private void showCompose(){
AccountSession session=AccountSessionManager.getInstance().getLastActiveAccount();
if(session==null || !session.activated)
return;
ComposeFragment compose=new ComposeFragment();
Bundle composeArgs=new Bundle();
composeArgs.putString("account", session.getID());
compose.setArguments(composeArgs);
showFragment(compose);
}
}

View File

@@ -233,6 +233,12 @@ public class CacheController{
});
}
public void deleteStatus(String id){
runOnDbThread((db)->{
db.delete("home_timeline", "`id`=?", new String[]{id});
});
}
public void clearRecentSearches(){
runOnDbThread((db)->db.delete("recent_searches", null, null));
}

View File

@@ -4,6 +4,7 @@ import android.os.Looper;
import org.joinmastodon.android.E;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.api.requests.statuses.SetStatusBookmarked;
import org.joinmastodon.android.api.requests.statuses.SetStatusFavorited;
import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
@@ -18,6 +19,7 @@ public class StatusInteractionController{
private final String accountID;
private final HashMap<String, SetStatusFavorited> runningFavoriteRequests=new HashMap<>();
private final HashMap<String, SetStatusReblogged> runningReblogRequests=new HashMap<>();
private final HashMap<String, SetStatusBookmarked> runningBookmarkRequests=new HashMap<>();
public StatusInteractionController(String accountID){
this.accountID=accountID;
@@ -61,6 +63,36 @@ public class StatusInteractionController{
E.post(new StatusCountersUpdatedEvent(status));
}
public void setBookmarked(Status status, boolean bookmarked){
if(!Looper.getMainLooper().isCurrentThread())
throw new IllegalStateException("Can only be called from main thread");
SetStatusBookmarked current=runningBookmarkRequests.remove(status.id);
if(current!=null){
current.cancel();
}
SetStatusBookmarked req=(SetStatusBookmarked) new SetStatusBookmarked(status.id, bookmarked)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Status result){
runningBookmarkRequests.remove(status.id);
E.post(new StatusCountersUpdatedEvent(result));
}
@Override
public void onError(ErrorResponse error){
runningBookmarkRequests.remove(status.id);
error.showToast(MastodonApp.context);
status.bookmarked=!bookmarked;
E.post(new StatusCountersUpdatedEvent(status));
}
})
.exec(accountID);
runningBookmarkRequests.put(status.id, req);
status.bookmarked=bookmarked;
E.post(new StatusCountersUpdatedEvent(status));
}
public void setReblogged(Status status, boolean reblogged){
if(!Looper.getMainLooper().isCurrentThread())
throw new IllegalStateException("Can only be called from main thread");

View File

@@ -21,6 +21,7 @@ public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
switch(filter){
case DEFAULT -> addQueryParameter("exclude_replies", "true");
case INCLUDE_REPLIES -> {}
case PINNED -> addQueryParameter("pinned", "true");
case MEDIA -> addQueryParameter("only_media", "true");
case NO_REBLOGS -> {
addQueryParameter("exclude_replies", "true");
@@ -32,6 +33,7 @@ public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
public enum Filter{
DEFAULT,
INCLUDE_REPLIES,
PINNED,
MEDIA,
NO_REBLOGS
}

View File

@@ -0,0 +1,52 @@
package org.joinmastodon.android.api.requests.accounts;
import androidx.annotation.NonNull;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import okhttp3.Response;
public class GetBookmarks extends MastodonAPIRequest<List<Status>>{
private String maxId;
public GetBookmarks(String maxID, String minID, int limit){
super(HttpMethod.GET, "/bookmarks", new TypeToken<>(){});
if(maxID!=null)
addQueryParameter("max_id", maxID);
if(minID!=null)
addQueryParameter("min_id", minID);
if(limit>0)
addQueryParameter("limit", ""+limit);
}
@Override
public void validateAndPostprocessResponse(List<Status> respObj, Response httpResponse) throws IOException {
super.validateAndPostprocessResponse(respObj, httpResponse);
// <https://mastodon.social/api/v1/bookmarks?max_id=268962>; rel="next",
// <https://mastodon.social/api/v1/bookmarks?min_id=268981>; rel="prev"
String link=httpResponse.header("link");
// parsing link header by hand; using a library would be cleaner
// (also, the functionality should be part of the max id logics and implemented in MastodonAPIRequest)
if(link==null) return;
String maxIdEq="max_id=";
for(String s : link.split(",")) {
if(s.contains("rel=\"next\"")) {
int start=s.indexOf(maxIdEq)+maxIdEq.length();
int end=s.indexOf('>');
if(start<0 || start>end) return;
this.maxId=s.substring(start, end);
}
}
}
public String getMaxId() {
return maxId;
}
}

View File

@@ -11,9 +11,9 @@ public class CreateOAuthApp extends MastodonAPIRequest<Application>{
}
private static class Request{
public String clientName="Mastodon for Android";
public String clientName="Mastadon for Android";
public String redirectUris=AccountSessionManager.REDIRECT_URI;
public String scopes=AccountSessionManager.SCOPE;
public String website="https://app.joinmastodon.org/android";
public String website="https://github.com/sk22/mastodon-android-fork";
}
}

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
public class SetStatusBookmarked extends MastodonAPIRequest<Status>{
public SetStatusBookmarked(String id, boolean bookmarked){
super(HttpMethod.POST, "/statuses/"+id+"/"+(bookmarked ? "bookmark" : "unbookmark"), Status.class);
setRequestBody(new Object());
}
}

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
public class SetStatusPinned extends MastodonAPIRequest<Status>{
public SetStatusPinned(String id, boolean pinned){
super(HttpMethod.POST, "/statuses/"+id+"/"+(pinned ? "pin" : "unpin"), Status.class);
setRequestBody(new Object());
}
}

View File

@@ -2,15 +2,22 @@ package org.joinmastodon.android.api.session;
import android.app.Activity;
import android.app.NotificationManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Build;
import android.util.Log;
import com.google.gson.JsonParseException;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
@@ -85,11 +92,12 @@ public class AccountSessionManager{
domains.add(session.domain.toLowerCase());
sessions.put(session.getID(), session);
}
}catch(IOException|JsonParseException x){
}catch(Exception x){
Log.e(TAG, "Error loading accounts", x);
}
lastActiveAccountID=prefs.getString("lastActiveAccount", null);
MastodonAPIController.runInBackground(()->readInstanceInfo(domains));
maybeUpdateShortcuts();
}
public void addAccount(Instance instance, Token token, Account self, Application app, boolean active){
@@ -102,6 +110,7 @@ public class AccountSessionManager{
if(PushSubscriptionManager.arePushNotificationsAvailable()){
session.getPushSubscriptionManager().registerAccountForPush(null);
}
maybeUpdateShortcuts();
}
public synchronized void writeAccountsFile(){
@@ -181,6 +190,7 @@ public class AccountSessionManager{
NotificationManager nm=MastodonApp.context.getSystemService(NotificationManager.class);
nm.deleteNotificationChannelGroup(id);
}
maybeUpdateShortcuts();
}
@NonNull
@@ -358,7 +368,7 @@ public class AccountSessionManager{
customEmojis.put(domain, groupCustomEmojis(emojis));
instances.put(domain, emojis.instance);
instancesLastUpdated.put(domain, emojis.lastUpdated);
}catch(IOException|JsonParseException x){
}catch(Exception x){
Log.w(TAG, "Error reading instance info file for "+domain, x);
}
}
@@ -395,6 +405,29 @@ public class AccountSessionManager{
writeAccountsFile();
}
private void maybeUpdateShortcuts(){
if(Build.VERSION.SDK_INT<26)
return;
ShortcutManager sm=MastodonApp.context.getSystemService(ShortcutManager.class);
if((sm.getDynamicShortcuts().isEmpty() || BuildConfig.DEBUG) && !sessions.isEmpty()){
// There are no shortcuts, but there are accounts. Add a compose shortcut.
ShortcutInfo info=new ShortcutInfo.Builder(MastodonApp.context, "compose")
.setActivity(ComponentName.createRelative(MastodonApp.context, MainActivity.class.getName()))
.setShortLabel(MastodonApp.context.getString(R.string.new_post))
.setIcon(Icon.createWithResource(MastodonApp.context, R.mipmap.ic_shortcut_compose))
.setIntent(new Intent(MastodonApp.context, MainActivity.class)
.setAction(Intent.ACTION_MAIN)
.putExtra("compose", true))
.build();
sm.setDynamicShortcuts(Collections.singletonList(info));
}else if(sessions.isEmpty()){
// There are shortcuts, but no accounts. Disable existing shortcuts.
sm.disableShortcuts(Collections.singletonList("compose"), MastodonApp.context.getString(R.string.err_not_logged_in));
}else{
sm.enableShortcuts(Collections.singletonList("compose"));
}
}
private static class SessionsStorageWrapper{
public List<AccountSession> accounts;
}

View File

@@ -5,7 +5,7 @@ import org.joinmastodon.android.model.Status;
public class StatusCountersUpdatedEvent{
public String id;
public int favorites, reblogs, replies;
public boolean favorited, reblogged;
public boolean favorited, reblogged, pinned;
public StatusCountersUpdatedEvent(Status s){
id=s.id;
@@ -14,5 +14,6 @@ public class StatusCountersUpdatedEvent{
replies=s.repliesCount;
favorited=s.favourited;
reblogged=s.reblogged;
pinned=s.pinned;
}
}

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.events;
public class StatusUnpinnedEvent {
public final String id;
public final String accountID;
public StatusUnpinnedEvent(String id, String accountID){
this.id=id;
this.accountID=accountID;
}
}

View File

@@ -8,8 +8,10 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusUnpinnedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.parceler.Parcels;
import java.util.Collections;
@@ -76,6 +78,7 @@ public class AccountTimelineFragment extends StatusListFragment{
protected void onStatusCreated(StatusCreatedEvent ev){
if(!AccountSessionManager.getInstance().isSelf(accountID, ev.status.account))
return;
if(filter==GetAccountStatuses.Filter.PINNED) return;
if(filter==GetAccountStatuses.Filter.DEFAULT){
// Keep replies to self, discard all other replies
if(ev.status.inReplyToAccountId!=null && !ev.status.inReplyToAccountId.equals(AccountSessionManager.getInstance().getAccount(accountID).self.id))
@@ -86,4 +89,24 @@ public class AccountTimelineFragment extends StatusListFragment{
}
prependItems(Collections.singletonList(ev.status), true);
}
protected void onStatusUnpinned(StatusUnpinnedEvent ev){
if(!ev.accountID.equals(accountID) || filter!=GetAccountStatuses.Filter.PINNED)
return;
Status status=getStatusByID(ev.id);
data.remove(status);
preloadedData.remove(status);
HeaderStatusDisplayItem item=findItemOfType(ev.id, HeaderStatusDisplayItem.class);
if(item==null)
return;
int index=displayItems.indexOf(item);
int lastIndex;
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
if(!displayItems.get(lastIndex).parentID.equals(ev.id))
break;
}
displayItems.subList(index, lastIndex).clear();
adapter.notifyItemRangeRemoved(index, lastIndex-index);
}
}

View File

@@ -461,9 +461,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class);
if(header!=null)
header.rebind();
for(ImageStatusDisplayItem.Holder photo:(List<ImageStatusDisplayItem.Holder>)findAllHoldersOfType(itemID, ImageStatusDisplayItem.Holder.class)){
photo.setRevealed(true);
}
updateImagesSpoilerState(status, itemID);
}
public void onVisibilityIconClick(HeaderStatusDisplayItem.Holder holder){
@@ -472,12 +470,25 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
if(!TextUtils.isEmpty(status.spoilerText)){
TextStatusDisplayItem.Holder text=findHolderOfType(holder.getItemID(), TextStatusDisplayItem.Holder.class);
if(text!=null){
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()+getMainAdapterOffset());
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition());
}
}
holder.rebind();
for(ImageStatusDisplayItem.Holder<?> photo:(List<ImageStatusDisplayItem.Holder>)findAllHoldersOfType(holder.getItemID(), ImageStatusDisplayItem.Holder.class)){
updateImagesSpoilerState(status, holder.getItemID());
}
protected void updateImagesSpoilerState(Status status, String itemID){
ArrayList<Integer> updatedPositions=new ArrayList<>();
for(ImageStatusDisplayItem.Holder photo:(List<ImageStatusDisplayItem.Holder>)findAllHoldersOfType(itemID, ImageStatusDisplayItem.Holder.class)){
photo.setRevealed(status.spoilerRevealed);
updatedPositions.add(photo.getAbsoluteAdapterPosition()-getMainAdapterOffset());
}
int i=0;
for(StatusDisplayItem item:displayItems){
if(itemID.equals(item.parentID) && item instanceof ImageStatusDisplayItem && !updatedPositions.contains(i)){
adapter.notifyItemChanged(i);
}
i++;
}
}

View File

@@ -0,0 +1,53 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetBookmarks;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Status;
import java.util.List;
import me.grishka.appkit.api.SimpleCallback;
public class BookmarksListFragment extends StatusListFragment{
private String accountID;
private Account self;
private String lastMaxId=null;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
self=session.self;
setTitle(R.string.bookmarks);
}
@Override
protected void onShown(){
super.onShown();
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
loadData();
}
@Override
protected void doLoadData(int offset, int count) {
GetBookmarks b=new GetBookmarks(offset>0 ? lastMaxId : null, null, count);
currentRequest=b.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
onDataLoaded(result, b.getMaxId()!=null);
lastMaxId=b.getMaxId();
}
})
.exec(accountID);
}
}

View File

@@ -22,6 +22,7 @@ import android.text.Layout;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.format.DateUtils;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
@@ -174,6 +175,14 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private Instance instance;
private boolean attachmentsErrorShowing;
public static DraftMediaAttachment redraftAttachment(Attachment att) {
DraftMediaAttachment draft=new DraftMediaAttachment();
draft.serverAttachment=att;
draft.description=att.description;
draft.uri=Uri.parse(att.url);
return draft;
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
@@ -204,6 +213,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
replyTo=Parcels.unwrap(getArguments().getParcelable("replyTo"));
statusVisibility=replyTo.visibility;
}
if(getArguments().containsKey("visibility")){
statusVisibility=(StatusPrivacy) getArguments().getSerializable("visibility");
}
if(savedInstanceState!=null){
statusVisibility=(StatusPrivacy) savedInstanceState.getSerializable("visibility");
}
@@ -286,11 +300,18 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
pollDurationView.setOnClickListener(v->showPollDurationMenu());
pollOptions.clear();
if(savedInstanceState!=null && savedInstanceState.containsKey("pollOptions")){
ArrayList<String> restoredPollOptions=(savedInstanceState!=null ? savedInstanceState : getArguments())
.getStringArrayList("pollOptions");
if(restoredPollOptions!=null){
if(savedInstanceState==null){
// restoring from arguments
pollDuration=getArguments().getInt("pollDuration");
pollDurationStr=DateUtils.formatElapsedTime(pollDuration); // getResources().getQuantityString(R.plurals.x_hours, pollDuration/3600);
}
pollBtn.setSelected(true);
mediaBtn.setEnabled(false);
pollWrap.setVisibility(View.VISIBLE);
for(String oldText:savedInstanceState.getStringArrayList("pollOptions")){
for(String oldText:restoredPollOptions){
DraftPollOption opt=createDraftPollOption();
opt.edit.setText(oldText);
}
@@ -310,8 +331,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
spoilerBtn.setSelected(true);
}
if(savedInstanceState!=null && savedInstanceState.containsKey("attachments")){
ArrayList<Parcelable> serializedAttachments=savedInstanceState.getParcelableArrayList("attachments");
ArrayList<Parcelable> serializedAttachments=(savedInstanceState!=null ? savedInstanceState : getArguments())
.getParcelableArrayList("attachments");
if(serializedAttachments!=null){
for(Parcelable a:serializedAttachments){
DraftMediaAttachment att=Parcels.unwrap(a);
attachmentsView.addView(createMediaAttachmentView(att));
@@ -453,11 +475,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if(savedInstanceState==null){
mainEditText.setText(initialText);
mainEditText.setSelection(mainEditText.length());
if(!TextUtils.isEmpty(replyTo.spoilerText) && AccountSessionManager.getInstance().isSelf(accountID, replyTo.account)){
// TODO: setting for preserving cw always / only when replying to own posts
// && AccountSessionManager.getInstance().isSelf(accountID, replyTo.account)
if(!TextUtils.isEmpty(replyTo.spoilerText)){
insertSpoiler(replyTo.spoilerText);
hasSpoiler=true;
spoilerEdit.setVisibility(View.VISIBLE);
spoilerEdit.setText(replyTo.spoilerText);
spoilerBtn.setSelected(true);
}
}
}else{
@@ -470,6 +492,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
mainEditText.setSelection(mainEditText.length());
initialText=prefilledText;
}
String spoilerText=getArguments().getString("spoilerText");
if(!TextUtils.isEmpty(spoilerText)) insertSpoiler(spoilerText);
ArrayList<Uri> mediaUris=getArguments().getParcelableArrayList("mediaAttachments");
if(mediaUris!=null && !mediaUris.isEmpty()){
for(Uri uri:mediaUris){
@@ -479,6 +503,13 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
}
private void insertSpoiler(String text) {
hasSpoiler=true;
if (text!=null) spoilerEdit.setText(text);
spoilerEdit.setVisibility(View.VISIBLE);
spoilerBtn.setSelected(true);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
publishButton=new Button(getActivity());
@@ -547,8 +578,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if(opt.edit.length()>0)
nonEmptyPollOptionsCount++;
}
publishButton.setEnabled((trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit && uploadingAttachment==null && failedAttachments.isEmpty() && queuedAttachments.isEmpty()
&& (pollOptions.isEmpty() || nonEmptyPollOptionsCount>1));
if(publishButton!=null){
publishButton.setEnabled((trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit
&& uploadingAttachment==null && failedAttachments.isEmpty() && queuedAttachments.isEmpty()
&& (pollOptions.isEmpty() || nonEmptyPollOptionsCount>1));
}
}
private void onCustomEmojiClick(Emoji emoji){
@@ -610,12 +644,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
public void onSuccess(Status result){
wm.removeView(sendingOverlay);
sendingOverlay=null;
Nav.finish(ComposeFragment.this);
E.post(new StatusCreatedEvent(result));
if(replyTo!=null){
replyTo.repliesCount++;
E.post(new StatusCountersUpdatedEvent(replyTo));
}
Nav.finish(ComposeFragment.this);
}
@Override
@@ -635,8 +669,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
boolean pollFieldsHaveContent=false;
for(DraftPollOption opt:pollOptions)
pollFieldsHaveContent|=opt.edit.length()>0;
return (mainEditText.length()>0 && !mainEditText.getText().toString().equals(initialText)) || !attachments.isEmpty()
|| uploadingAttachment!=null || !queuedAttachments.isEmpty() || !failedAttachments.isEmpty() || pollFieldsHaveContent;
return getArguments().getBoolean("hasDraft", false)
|| (mainEditText.length()>0 && !mainEditText.getText().toString().equals(initialText))
|| !attachments.isEmpty() || uploadingAttachment!=null || !queuedAttachments.isEmpty()
|| !failedAttachments.isEmpty() || pollFieldsHaveContent;
}
@Override

View File

@@ -255,9 +255,14 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
@Override
public boolean onBackPressed(){
if(currentTab==R.id.tab_profile)
return profileFragment.onBackPressed();
if (profileFragment.onBackPressed()) return true;
if(currentTab==R.id.tab_search)
return searchFragment.onBackPressed();
if (searchFragment.onBackPressed()) return true;
if (currentTab!=R.id.tab_home) {
tabBar.selectTab(R.id.tab_home);
onTabSelected(R.id.tab_home);
return true;
}
return false;
}

View File

@@ -101,7 +101,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private ProgressBarButton actionButton;
private ViewPager2 pager;
private NestedRecyclerScrollView scrollView;
private AccountTimelineFragment postsFragment, postsWithRepliesFragment, mediaFragment;
private AccountTimelineFragment postsFragment, postsWithRepliesFragment, pinnedPostsFragment, mediaFragment;
private ProfileAboutFragment aboutFragment;
private TabLayout tabbar;
private SwipeRefreshLayout refreshLayout;
@@ -209,14 +209,15 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
};
tabViews=new FrameLayout[4];
tabViews=new FrameLayout[5];
for(int i=0;i<tabViews.length;i++){
FrameLayout tabView=new FrameLayout(getActivity());
tabView.setId(switch(i){
case 0 -> R.id.profile_posts;
case 1 -> R.id.profile_posts_with_replies;
case 2 -> R.id.profile_media;
case 3 -> R.id.profile_about;
case 2 -> R.id.profile_pinned_posts;
case 3 -> R.id.profile_media;
case 4 -> R.id.profile_about;
default -> throw new IllegalStateException("Unexpected value: "+i);
});
tabView.setVisibility(View.GONE);
@@ -224,7 +225,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
tabViews[i]=tabView;
}
pager.setOffscreenPageLimit(4);
pager.setOffscreenPageLimit(5);
pager.setAdapter(new ProfilePagerAdapter());
pager.getLayoutParams().height=getResources().getDisplayMetrics().heightPixels;
@@ -240,8 +241,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
tab.setText(switch(position){
case 0 -> R.string.posts;
case 1 -> R.string.posts_and_replies;
case 2 -> R.string.media;
case 3 -> R.string.profile_about;
case 2 -> R.string.pinned_posts;
case 3 -> R.string.media;
case 4 -> R.string.profile_about;
default -> throw new IllegalStateException();
});
}
@@ -298,6 +300,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
postsFragment.onRefresh();
if(postsWithRepliesFragment.loaded)
postsWithRepliesFragment.onRefresh();
if(pinnedPostsFragment.loaded)
pinnedPostsFragment.onRefresh();
if(mediaFragment.loaded)
mediaFragment.onRefresh();
}
@@ -322,6 +326,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(postsFragment==null){
postsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.DEFAULT, true);
postsWithRepliesFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.INCLUDE_REPLIES, false);
pinnedPostsFragment =AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.PINNED, false);
mediaFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.MEDIA, false);
aboutFragment=new ProfileAboutFragment();
aboutFragment.setFields(fields);
@@ -402,6 +407,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(postsFragment!=null && postsFragment.isAdded() && childInsets!=null){
postsFragment.onApplyWindowInsets(childInsets);
postsWithRepliesFragment.onApplyWindowInsets(childInsets);
pinnedPostsFragment.onApplyWindowInsets(childInsets);
mediaFragment.onApplyWindowInsets(childInsets);
}
}
@@ -509,10 +515,17 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
return;
}
if(relationship==null)
if(relationship==null && !isOwnProfile)
return;
inflater.inflate(R.menu.profile, menu);
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getDisplayUsername()));
if(isOwnProfile){
for(int i=0;i<menu.size();i++){
MenuItem item=menu.getItem(i);
item.setVisible(item.getItemId()==R.id.share || item.getItemId()==R.id.bookmarks);
}
return;
}
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()));
@@ -529,11 +542,16 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@Override
public boolean onOptionsItemSelected(MenuItem item){
int id=item.getItemId();
if(id==R.id.share){
Intent intent=new Intent(Intent.ACTION_SEND);
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.bookmarks) {
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(account));
Nav.go(getActivity(), BookmarksListFragment.class, args);
}else if(id==R.id.mute){
confirmToggleMuted();
}else if(id==R.id.block){
@@ -637,8 +655,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
return switch(page){
case 0 -> postsFragment;
case 1 -> postsWithRepliesFragment;
case 2 -> mediaFragment;
case 3 -> aboutFragment;
case 2 -> pinnedPostsFragment;
case 3 -> mediaFragment;
case 4 -> aboutFragment;
default -> throw new IllegalStateException();
};
}
@@ -699,9 +718,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
invalidateOptionsMenu();
pager.setUserInputEnabled(false);
actionButton.setText(R.string.done);
pager.setCurrentItem(3);
pager.setCurrentItem(4);
ArrayList<Animator> animators=new ArrayList<>();
for(int i=0;i<3;i++){
for(int i=0;i<tabViews.length-1;i++){
animators.add(ObjectAnimator.ofFloat(tabbar.getTabAt(i).view, View.ALPHA, .3f));
tabbar.getTabAt(i).view.setEnabled(false);
}
@@ -742,7 +761,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
invalidateOptionsMenu();
ArrayList<Animator> animators=new ArrayList<>();
actionButton.setText(R.string.edit_profile);
for(int i=0;i<3;i++){
for(int i=0;i<tabViews.length-1;i++){
animators.add(ObjectAnimator.ofFloat(tabbar.getTabAt(i).view, View.ALPHA, 1f));
}
animators.add(ObjectAnimator.ofInt(avatar.getForeground(), "alpha", 0));
@@ -760,7 +779,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
for(int i=0;i<3;i++){
for(int i=0;i<tabViews.length-1;i++){
tabbar.getTabAt(i).view.setEnabled(true);
}
pager.setUserInputEnabled(true);
@@ -937,7 +956,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@Override
public int getItemCount(){
return loaded ? 4 : 0;
return loaded ? tabViews.length : 0;
}
@Override

View File

@@ -9,10 +9,10 @@ import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusDeletedEvent;
import org.joinmastodon.android.events.StatusUnpinnedEvent;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.parceler.Parcels;
@@ -61,6 +61,8 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
protected void onStatusCreated(StatusCreatedEvent ev){}
protected void onStatusUnpinned(StatusUnpinnedEvent ev){}
protected Status getContentStatusByID(String id){
Status s=getStatusByID(id);
return s==null ? null : s.getContentStatus();
@@ -113,10 +115,15 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
return;
data.remove(status);
preloadedData.remove(status);
HeaderStatusDisplayItem item=findItemOfType(ev.id, HeaderStatusDisplayItem.class);
if(item==null)
int index=-1;
for(int i=0;i<displayItems.size();i++){
if(ev.id.equals(displayItems.get(i).parentID)){
index=i;
break;
}
}
if(index==-1)
return;
int index=displayItems.indexOf(item);
int lastIndex;
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
if(!displayItems.get(lastIndex).parentID.equals(ev.id))
@@ -131,6 +138,11 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
StatusListFragment.this.onStatusCreated(ev);
}
@Subscribe
public void onStatusUnpinned(StatusUnpinnedEvent ev){
StatusListFragment.this.onStatusUnpinned(ev);
}
@Subscribe
public void onPollUpdated(PollUpdatedEvent ev){
if(!ev.accountID.equals(accountID))

View File

@@ -157,6 +157,18 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
}
});
tabLayoutMediator.attach();
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener(){
@Override
public void onTabSelected(TabLayout.Tab tab){}
@Override
public void onTabUnselected(TabLayout.Tab tab){}
@Override
public void onTabReselected(TabLayout.Tab tab){
scrollToTop();
}
});
searchEdit=view.findViewById(R.id.search_edit);
searchEdit.setOnFocusChangeListener(this::onSearchEditFocusChanged);

View File

@@ -205,7 +205,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
@Override
public void onTabReselected(TabLayout.Tab tab){
scrollToTop();
}
});
}

View File

@@ -126,6 +126,7 @@ public class Status extends BaseModel implements DisplayItemsParent{
repliesCount=ev.replies;
favourited=ev.favorited;
reblogged=ev.reblogged;
pinned=ev.pinned;
}
public Status getContentStatus(){

View File

@@ -162,6 +162,7 @@ public class ComposeAutocompleteViewController{
.map(WrappedEmoji::new)
.collect(Collectors.toList());
UiUtils.updateList(oldList, emojis, list, emojisAdapter, (e1, e2)->e1.emoji.shortcode.equals(e2.emoji.shortcode));
imgLoader.updateImages();
}
}
@@ -186,6 +187,7 @@ public class ComposeAutocompleteViewController{
List<WrappedAccount> oldList=users;
users=result.accounts.stream().map(WrappedAccount::new).collect(Collectors.toList());
UiUtils.updateList(oldList, users, list, usersAdapter, (a1, a2)->a1.account.id.equals(a2.account.id));
imgLoader.updateImages();
if(listIsHidden){
listIsHidden=false;
V.setVisibilityAnimated(list, View.VISIBLE);
@@ -210,6 +212,7 @@ public class ComposeAutocompleteViewController{
List<Hashtag> oldList=hashtags;
hashtags=result.hashtags;
UiUtils.updateList(oldList, hashtags, list, hashtagsAdapter, (t1, t2)->t1.name.equals(t2.name));
imgLoader.updateImages();
if(listIsHidden){
listIsHidden=false;
V.setVisibilityAnimated(list, View.VISIBLE);

View File

@@ -92,7 +92,9 @@ public class AudioStatusDisplayItem extends StatusDisplayItem{
public void onBind(AudioStatusDisplayItem item){
int seconds=(int)item.attachment.getDuration();
String duration=formatDuration(seconds);
time.getLayoutParams().width=(int)Math.ceil(time.getPaint().measureText("-"+duration));
// Some fonts (not Roboto) have different-width digits. 0 is supposedly the widest.
time.getLayoutParams().width=(int)Math.ceil(Math.max(time.getPaint().measureText("-"+duration),
time.getPaint().measureText("-"+duration.replaceAll("\\d", "0"))));
time.setText(duration);
AudioPlayerService service=AudioPlayerService.getInstance();
if(service!=null && service.getAttachmentID().equals(item.attachment.id)){

View File

@@ -43,7 +43,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
}
public static class Holder extends StatusDisplayItem.Holder<FooterStatusDisplayItem>{
private final TextView reply, boost, favorite;
private final TextView reply, boost, favorite, bookmark;
private final ImageView share;
private final View.AccessibilityDelegate buttonAccessibilityDelegate=new View.AccessibilityDelegate(){
@@ -60,22 +60,27 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
reply=findViewById(R.id.reply);
boost=findViewById(R.id.boost);
favorite=findViewById(R.id.favorite);
bookmark=findViewById(R.id.bookmark);
share=findViewById(R.id.share);
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N){
UiUtils.fixCompoundDrawableTintOnAndroid6(reply);
UiUtils.fixCompoundDrawableTintOnAndroid6(boost);
UiUtils.fixCompoundDrawableTintOnAndroid6(favorite);
UiUtils.fixCompoundDrawableTintOnAndroid6(bookmark);
}
View reply=findViewById(R.id.reply_btn);
View boost=findViewById(R.id.boost_btn);
View favorite=findViewById(R.id.favorite_btn);
View share=findViewById(R.id.share_btn);
View bookmark=findViewById(R.id.bookmark_btn);
reply.setOnClickListener(this::onReplyClick);
reply.setAccessibilityDelegate(buttonAccessibilityDelegate);
boost.setOnClickListener(this::onBoostClick);
boost.setAccessibilityDelegate(buttonAccessibilityDelegate);
favorite.setOnClickListener(this::onFavoriteClick);
favorite.setAccessibilityDelegate(buttonAccessibilityDelegate);
bookmark.setOnClickListener(this::onBookmarkClick);
bookmark.setAccessibilityDelegate(buttonAccessibilityDelegate);
share.setOnClickListener(this::onShareClick);
share.setAccessibilityDelegate(buttonAccessibilityDelegate);
}
@@ -87,6 +92,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
bindButton(favorite, item.status.favouritesCount);
boost.setSelected(item.status.reblogged);
favorite.setSelected(item.status.favourited);
bookmark.setSelected(item.status.bookmarked);
boost.setEnabled(item.status.visibility==StatusPrivacy.PUBLIC || item.status.visibility==StatusPrivacy.UNLISTED
|| (item.status.visibility==StatusPrivacy.PRIVATE && item.status.account.id.equals(AccountSessionManager.getInstance().getAccount(item.accountID).self.id)));
}
@@ -120,6 +126,11 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
bindButton(favorite, item.status.favouritesCount);
}
private void onBookmarkClick(View v){
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setBookmarked(item.status, !item.status.bookmarked);
bookmark.setSelected(item.status.bookmarked);
}
private void onShareClick(View v){
Intent intent=new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
@@ -134,6 +145,8 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
return R.string.button_reblog;
if(id==R.id.favorite_btn)
return R.string.button_favorite;
if(id==R.id.bookmark_btn)
return R.string.button_bookmark;
if(id==R.id.share_btn)
return R.string.button_share;
return 0;

View File

@@ -137,6 +137,10 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
int id=menuItem.getItemId();
if(id==R.id.delete){
UiUtils.confirmDeletePost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{});
}else if(id==R.id.pin || id==R.id.unpin){
UiUtils.confirmPinPost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, !item.status.pinned, s->{});
}else if(id==R.id.delete_and_redraft) {
UiUtils.confirmDeleteAndRedraftPost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{});
}else if(id==R.id.mute){
UiUtils.confirmToggleMuteUser(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), account, relationship!=null && relationship.muting, r->{});
}else if(id==R.id.block){
@@ -250,6 +254,8 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
Menu menu=optionsMenu.getMenu();
boolean isOwnPost=AccountSessionManager.getInstance().isSelf(item.parentFragment.getAccountID(), account);
menu.findItem(R.id.delete).setVisible(item.status!=null && isOwnPost);
menu.findItem(R.id.pin).setVisible(item.status!=null && isOwnPost && !item.status.pinned);
menu.findItem(R.id.unpin).setVisible(item.status!=null && isOwnPost && item.status.pinned);
menu.findItem(R.id.open_in_browser).setVisible(item.status!=null);
MenuItem blockDomain=menu.findItem(R.id.block_domain);
MenuItem mute=menu.findItem(R.id.mute);

View File

@@ -11,6 +11,7 @@ import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import org.joinmastodon.android.ui.views.LinkedTextView;
@@ -20,7 +21,8 @@ import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
public class TextStatusDisplayItem extends StatusDisplayItem{
private CharSequence text;
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper(), spoilerEmojiHelper;
private CharSequence parsedSpoilerText;
public boolean textSelectable;
public final Status status;
@@ -29,6 +31,11 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
this.text=text;
this.status=status;
emojiHelper.setText(text);
if(!TextUtils.isEmpty(status.spoilerText)){
parsedSpoilerText=HtmlParser.parseCustomEmoji(status.spoilerText, status.emojis);
spoilerEmojiHelper=new CustomEmojiHelper();
spoilerEmojiHelper.setText(parsedSpoilerText);
}
}
@Override
@@ -38,11 +45,15 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
@Override
public int getImageCount(){
if(spoilerEmojiHelper!=null && !status.spoilerRevealed)
return spoilerEmojiHelper.getImageCount();
return emojiHelper.getImageCount();
}
@Override
public ImageLoaderRequest getImageRequest(int index){
if(spoilerEmojiHelper!=null && !status.spoilerRevealed)
return spoilerEmojiHelper.getImageRequest(index);
return emojiHelper.getImageRequest(index);
}
@@ -65,7 +76,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
text.setTextIsSelectable(item.textSelectable);
text.setInvalidateOnEveryFrame(false);
if(!TextUtils.isEmpty(item.status.spoilerText)){
spoilerTitle.setText(item.status.spoilerText);
spoilerTitle.setText(item.parsedSpoilerText);
if(item.status.spoilerRevealed){
spoilerOverlay.setVisibility(View.GONE);
text.setVisibility(View.VISIBLE);
@@ -84,8 +95,9 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
@Override
public void setImage(int index, Drawable image){
item.emojiHelper.setImageDrawable(index, image);
getEmojiHelper().setImageDrawable(index, image);
text.invalidate();
spoilerTitle.invalidate();
if(image instanceof Animatable){
((Animatable) image).start();
if(image instanceof MovieDrawable)
@@ -95,8 +107,12 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
@Override
public void clearImage(int index){
item.emojiHelper.setImageDrawable(index, null);
getEmojiHelper().setImageDrawable(index, null);
text.invalidate();
}
private CustomEmojiHelper getEmojiHelper(){
return item.spoilerEmojiHelper!=null && !item.status.spoilerRevealed ? item.spoilerEmojiHelper : item.emojiHelper;
}
}
}

View File

@@ -40,7 +40,6 @@ import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.PopupWindow;
import android.widget.ProgressBar;
import android.widget.SeekBar;
import android.widget.TextView;
@@ -57,7 +56,6 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
@@ -73,7 +71,6 @@ import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.BottomSheet;
import me.grishka.appkit.views.FragmentRootLinearLayout;
import okio.BufferedSink;
import okio.Okio;

View File

@@ -0,0 +1,8 @@
package org.joinmastodon.android.ui.text;
/**
* A span to mark character ranges that should be deleted when copied to the clipboard.
* Works with {@link org.joinmastodon.android.ui.views.LinkedTextView}.
*/
public class DeleteWhenCopiedSpan{
}

View File

@@ -67,10 +67,9 @@ public class HtmlParser{
@Override
public void head(@NonNull Node node, int depth){
if(node instanceof TextNode){
ssb.append(((TextNode) node).text());
}else if(node instanceof Element){
Element el=(Element)node;
if(node instanceof TextNode textNode){
ssb.append(textNode.text());
}else if(node instanceof Element el){
switch(el.nodeName()){
case "a" -> {
String href=el.attr("href");
@@ -108,10 +107,9 @@ public class HtmlParser{
@Override
public void tail(@NonNull Node node, int depth){
if(node instanceof Element){
Element el=(Element)node;
if(node instanceof Element el){
if("span".equals(el.nodeName()) && el.hasClass("ellipsis")){
ssb.append('…');
ssb.append("", new DeleteWhenCopiedSpan(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}else if("p".equals(el.nodeName())){
if(node.nextSibling()!=null)
ssb.append("\n\n");

View File

@@ -11,7 +11,6 @@ import android.content.res.TypedArray;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
@@ -20,6 +19,7 @@ import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Parcelable;
import android.provider.OpenableColumns;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
@@ -42,8 +42,13 @@ import org.joinmastodon.android.api.requests.accounts.SetAccountMuted;
import org.joinmastodon.android.api.requests.accounts.SetDomainBlocked;
import org.joinmastodon.android.api.requests.statuses.DeleteStatus;
import org.joinmastodon.android.api.requests.statuses.GetStatusByID;
import org.joinmastodon.android.api.requests.statuses.SetStatusPinned;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.StatusDeletedEvent;
import org.joinmastodon.android.events.StatusUnpinnedEvent;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ThreadFragment;
@@ -51,8 +56,10 @@ import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.text.SpacerSpan;
import org.parceler.Parcels;
@@ -62,6 +69,8 @@ import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@@ -74,6 +83,10 @@ import androidx.annotation.StringRes;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
@@ -107,7 +120,9 @@ public class UiUtils{
long t=instant.toEpochMilli();
long now=System.currentTimeMillis();
long diff=now-t;
if(diff<60_000L){
if(diff<1000L){
return context.getString(R.string.time_now);
}else if(diff<60_000L){
return context.getString(R.string.time_seconds, diff/1000L);
}else if(diff<3600_000L){
return context.getString(R.string.time_minutes, diff/60_000L);
@@ -336,6 +351,7 @@ public class UiUtils{
@Override
public void onSuccess(Status result){
resultCallback.accept(result);
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().deleteStatus(status.id);
E.post(new StatusDeletedEvent(status.id, accountID));
}
@@ -349,6 +365,88 @@ public class UiUtils{
});
}
public static void confirmPinPost(Activity activity, String accountID, Status status, boolean pinned, Consumer<Status> resultCallback){
showConfirmationAlert(activity,
pinned ? R.string.confirm_pin_post_title : R.string.confirm_unpin_post_title,
pinned ? R.string.confirm_pin_post : R.string.confirm_unpin_post,
pinned ? R.string.pin_post : R.string.unpin_post,
()->{
new SetStatusPinned(status.id, pinned)
.setCallback(new Callback<>() {
@Override
public void onSuccess(Status result) {
resultCallback.accept(result);
E.post(new StatusCountersUpdatedEvent(result));
if (!result.pinned)
E.post(new StatusUnpinnedEvent(status.id, accountID));
}
@Override
public void onError(ErrorResponse error) {
error.showToast(activity);
}
})
.wrapProgress(activity, pinned ? R.string.pinning : R.string.unpinning, false)
.exec(accountID);
}
);
}
public static void confirmDeleteAndRedraftPost(Activity activity, String accountID, Status status, Consumer<Status> resultCallback){
showConfirmationAlert(activity, R.string.confirm_delete_and_redraft_title, R.string.confirm_delete_and_redraft, R.string.delete_and_redraft, ()->{
new DeleteStatus(status.id)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Status result){
resultCallback.accept(result);
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().deleteStatus(status.id);
E.post(new StatusDeletedEvent(status.id, accountID));
UiUtils.redraftStatus(status, accountID, activity);
}
@Override
public void onError(ErrorResponse error){
error.showToast(activity);
}
})
.wrapProgress(activity, R.string.deleting, false)
.exec(accountID);
});
}
public static void redraftStatus(Status status, String accountID, Activity activity) {
Bundle args=new Bundle();
args.putString("account", accountID);
args.putBoolean("hasDraft", true);
args.putString("prefilledText", HtmlParser.parse(status.content, status.emojis, status.mentions, status.tags, accountID).toString());
args.putString("spoilerText", status.spoilerText);
args.putSerializable("visibility", status.visibility);
if(status.poll!=null){
args.putInt("pollDuration", (int)status.poll.expiresAt.minus(status.createdAt.getEpochSecond(), ChronoUnit.SECONDS).getEpochSecond());
ArrayList<String> opts=status.poll.options.stream().map(o -> o.title).collect(Collectors.toCollection(ArrayList::new));
args.putStringArrayList("pollOptions", opts);
}
if(!status.mediaAttachments.isEmpty()){
ArrayList<Parcelable> serializedAttachments=status.mediaAttachments.stream()
.map(att -> Parcels.wrap(ComposeFragment.redraftAttachment(att)))
.collect(Collectors.toCollection(ArrayList::new));
args.putParcelableArrayList("attachments", serializedAttachments);
}
Callback<Status> cb=new Callback<>(){
@Override public void onError(ErrorResponse error) {
onSuccess(null);
error.showToast(activity);
}
@Override public void onSuccess(Status status) {
if (status!=null) args.putParcelable("replyTo", Parcels.wrap(status));
Nav.go(activity, ComposeFragment.class, args);
}
};
if(status.inReplyToId!=null) new GetStatusByID(status.inReplyToId).setCallback(cb).exec(accountID);
else cb.onSuccess(null);
}
public static void setRelationshipToActionButton(Relationship relationship, Button button){
boolean secondaryStyle;
if(relationship.blocking){

View File

@@ -36,7 +36,7 @@ public class ImageAttachmentFrameLayout extends FrameLayout{
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
int w=Math.min(((View)getParent()).getMeasuredWidth()-horizontalInset, V.dp(MAX_WIDTH));
int w=Math.min(((View)getParent()).getMeasuredWidth(), V.dp(MAX_WIDTH))-horizontalInset;
int actualHeight=Math.round(tile.height/1000f*w)+V.dp(1)*(tile.rowSpan-1);
int actualWidth=Math.round(tile.width/1000f*w);
if(tile.startCol+tile.colSpan<tileLayout.columnSizes.length)

View File

@@ -1,38 +1,68 @@
package org.joinmastodon.android.ui.views;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.graphics.Canvas;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.widget.TextView;
import org.joinmastodon.android.ui.text.ClickableLinksDelegate;
import org.joinmastodon.android.ui.text.DeleteWhenCopiedSpan;
public class LinkedTextView extends TextView {
public class LinkedTextView extends TextView{
private ClickableLinksDelegate delegate=new ClickableLinksDelegate(this);
private boolean needInvalidate;
public LinkedTextView(Context context) {
super(context);
// TODO Auto-generated constructor stub
private ActionMode currentActionMode;
public LinkedTextView(Context context){
this(context, null);
}
public LinkedTextView(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
public LinkedTextView(Context context, AttributeSet attrs){
this(context, attrs, 0);
}
public LinkedTextView(Context context, AttributeSet attrs, int defStyle) {
public LinkedTextView(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
// TODO Auto-generated constructor stub
setCustomSelectionActionModeCallback(new ActionMode.Callback(){
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu){
currentActionMode=mode;
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu){
return true;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item){
onTextContextMenuItem(item.getItemId());
return true;
}
@Override
public void onDestroyActionMode(ActionMode mode){
currentActionMode=null;
}
});
}
public boolean onTouchEvent(MotionEvent ev){
if(delegate.onTouch(ev)) return true;
return super.onTouchEvent(ev);
return super.onTouchEvent(ev);
}
public void onDraw(Canvas c){
super.onDraw(c);
delegate.onDraw(c);
@@ -47,4 +77,43 @@ public class LinkedTextView extends TextView {
invalidate();
}
@Override
public boolean onTextContextMenuItem(int id){
if(id==android.R.id.copy){
final int selStart=getSelectionStart();
final int selEnd=getSelectionEnd();
int min=Math.max(0, Math.min(selStart, selEnd));
int max=Math.max(0, Math.max(selStart, selEnd));
final ClipData copyData=ClipData.newPlainText(null, deleteTextWithinDeleteSpans(getText().subSequence(min, max)));
ClipboardManager clipboard=getContext().getSystemService(ClipboardManager.class);
try {
clipboard.setPrimaryClip(copyData);
} catch (Throwable t) {
Log.w("LinkedTextView", t);
}
if(currentActionMode!=null){
currentActionMode.finish();
}
return true;
}
return super.onTextContextMenuItem(id);
}
private CharSequence deleteTextWithinDeleteSpans(CharSequence text){
if(text instanceof Spanned spanned){
DeleteWhenCopiedSpan[] delSpans=spanned.getSpans(0, text.length(), DeleteWhenCopiedSpan.class);
if(delSpans.length>0){
SpannableStringBuilder ssb=new SpannableStringBuilder(spanned);
for(DeleteWhenCopiedSpan span:delSpans){
int start=ssb.getSpanStart(span);
int end=ssb.getSpanStart(span);
if(start==-1)
continue;
ssb.delete(start, end+1);
}
return ssb;
}
}
return text;
}
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/bookmark_selected" android:state_selected="true"/>
<item android:color="?android:textColorSecondary"/>
</selector>

View File

@@ -0,0 +1,7 @@
<vector android:height="108dp"
android:viewportHeight="48" android:viewportWidth="48"
android:width="108dp" xmlns:android="http://schemas.android.com/apk/res/android">
<group android:translateX="12" android:translateY="12">
<path android:fillColor="@color/shortcut_icon_foreground" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</group>
</vector>

View File

@@ -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="M6.19 21.855c-0.495 0.357-1.187 0.002-1.187-0.61V6.25C5.003 4.455 6.458 3 8.253 3h7.498c1.795 0 3.25 1.455 3.25 3.25v14.996c0 0.611-0.692 0.966-1.188 0.609l-5.81-4.181-5.812 4.18z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -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="M6.19 21.855c-0.495 0.357-1.187 0.002-1.187-0.61V6.25C5.003 4.455 6.458 3 8.253 3h7.498c1.795 0 3.25 1.455 3.25 3.25v14.996c0 0.611-0.692 0.966-1.188 0.609l-5.81-4.181-5.812 4.18zM17.502 6.25c0-0.966-0.783-1.75-1.75-1.75H8.253c-0.967 0-1.75 0.784-1.75 1.75v13.532l5.061-3.641c0.262-0.188 0.614-0.188 0.876 0l5.061 3.641V6.25z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!--~ Copyright (c) 2022. ~ Microsoft Corporation. All rights reserved.-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_fluent_bookmark_24_filled" android:state_activated="true"/>
<item android:drawable="@drawable/ic_fluent_bookmark_24_filled" android:state_checked="true"/>
<item android:drawable="@drawable/ic_fluent_bookmark_24_filled" android:state_selected="true"/>
<item android:drawable="@drawable/ic_fluent_bookmark_24_regular"/>
</selector>

View File

@@ -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="M4 6.748c0-1.243 1.007-2.25 2.25-2.25h9c1.243 0 2.25 1.007 2.25 2.25V21.25c0 0.268-0.143 0.517-0.376 0.65-0.233 0.134-0.52 0.133-0.751-0.002l-5.623-3.28-5.622 3.28c-0.232 0.135-0.519 0.136-0.752 0.002C4.144 21.767 4 21.52 4 21.25V6.748zM15.25 2C17.873 2 20 4.127 20 6.75v11.873c0 0.414-0.336 0.75-0.75 0.75s-0.75-0.336-0.75-0.75V6.751c0-1.796-1.455-3.25-3.25-3.25H6.637S6.75 2.942 7.434 2.42C8 2 8.602 2 8.602 2h6.648z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -46,6 +46,7 @@
android:textAppearance="@style/m3_label_medium"
android:textColor="?colorButtonText"
android:gravity="end"
android:singleLine="true"
tools:text="1:23"/>
</LinearLayout>

View File

@@ -73,6 +73,28 @@
tools:text="123"/>
</FrameLayout>
<Space
android:layout_width="0px"
android:layout_height="1px"
android:layout_weight="1"/>
<FrameLayout
android:id="@+id/bookmark_btn"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:minWidth="56dp">
<TextView
android:id="@+id/bookmark"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_gravity="center"
android:drawableStart="@drawable/ic_fluent_bookmark_24_selector"
android:drawablePadding="8dp"
android:drawableTint="@color/bookmark_icon"
android:gravity="center_vertical"
android:textAppearance="@style/m3_label_large" />
</FrameLayout>
<Space
android:layout_width="0px"
android:layout_height="1px"

View File

@@ -9,20 +9,21 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<org.joinmastodon.android.ui.views.ComposeMediaLayout
android:layout_width="wrap_content"
<org.joinmastodon.android.ui.views.MaxWidthFrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal">
android:layout_gravity="center"
android:maxWidth="400dp">
<ImageView
android:id="@+id/photo"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:importantForAccessibility="no"
tools:src="#0f0"/>
</org.joinmastodon.android.ui.views.ComposeMediaLayout>
</org.joinmastodon.android.ui.views.MaxWidthFrameLayout>
<TextView
android:id="@+id/title"

View File

@@ -78,102 +78,110 @@
tools:text="Founder, CEO and lead developer @Mastodon, Germany." />
<LinearLayout
android:id="@+id/posts_btn"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_below="@id/bio"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:id="@+id/posts_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_large"
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"
tools:text="following" />
</LinearLayout>
<LinearLayout
android:id="@+id/followers_btn"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_toEndOf="@id/posts_btn"
android:layout_alignTop="@id/posts_btn"
android:layout_marginStart="12dp"
android:orientation="vertical"
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"
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>
<LinearLayout
android:id="@+id/following_btn"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_alignTop="@id/posts_btn"
android:layout_toEndOf="@id/followers_btn"
android:layout_marginStart="12dp"
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>
<FrameLayout
android:id="@+id/action_btn_wrap"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignTop="@id/posts_btn"
android:layout_marginTop="-8dp"
android:padding="8dp"
android:layout_marginEnd="8dp"
android:clipToPadding="false">
<org.joinmastodon.android.ui.views.ProgressBarButton
android:id="@+id/action_btn"
android:layout_below="@id/bio"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/posts_btn"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:id="@+id/posts_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_large"
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"
tools:text="following" />
</LinearLayout>
<LinearLayout
android:id="@+id/followers_btn"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginTop="16dp"
android:layout_marginStart="12dp"
android:orientation="vertical"
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"
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>
<LinearLayout
android:id="@+id/following_btn"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginTop="16dp"
android:layout_marginStart="12dp"
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>
<Space
android:layout_width="0px"
android:layout_height="1px"
android:layout_weight="1"/>
<FrameLayout
android:id="@+id/action_btn_wrap"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Edit Profile"/>
<ProgressBar
android:id="@+id/action_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
style="?android:progressBarStyleSmall"
android:elevation="10dp"
android:outlineProvider="none"
android:indeterminateTint="?colorButtonText"
android:visibility="gone"/>
</FrameLayout>
android:layout_marginTop="8dp"
android:padding="8dp"
android:layout_marginEnd="8dp"
android:clipToPadding="false">
<org.joinmastodon.android.ui.views.ProgressBarButton
android:id="@+id/action_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
tools:text="@string/follow_back"/>
<ProgressBar
android:id="@+id/action_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
style="?android:progressBarStyleSmall"
android:elevation="10dp"
android:outlineProvider="none"
android:indeterminateTint="?colorButtonText"
android:visibility="gone"/>
</FrameLayout>
</LinearLayout>
</RelativeLayout>

View File

@@ -1,6 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/delete" android:title="@string/delete"/>
<item android:id="@+id/delete_and_redraft" android:title="@string/delete_and_redraft"/>
<item android:id="@+id/pin" android:title="@string/pin_post"/>
<item android:id="@+id/unpin" android:title="@string/unpin_post"/>
<item android:id="@+id/mute" android:title="@string/mute_user"/>
<item android:id="@+id/block" android:title="@string/block_user"/>
<item android:id="@+id/block_domain" android:title="@string/block_domain"/>

View File

@@ -7,4 +7,10 @@
<item android:id="@+id/block_domain" android:title="@string/block_domain"/>
<item android:id="@+id/hide_boosts" android:title="@string/hide_boosts_from_user"/>
<item android:id="@+id/open_in_browser" android:title="@string/open_in_browser"/>
<item
android:id="@+id/bookmarks"
android:showAsAction="always"
android:visible="false"
android:icon="@drawable/ic_fluent_bookmark_multiple_24_filled"
android:title="@string/bookmarks"/>
</menu>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background>
<shape>
<solid android:color="@color/shortcut_icon_background"/>
<size android:width="108dp" android:height="108dp"/>
</shape>
</background>
<foreground android:drawable="@drawable/ic_compose_foreground"/>
</adaptive-icon>

View File

@@ -42,6 +42,7 @@
</plurals>
<string name="posts">Beiträge</string>
<string name="posts_and_replies">Beiträge und Antworten</string>
<string name="pinned_posts">Angeheftet</string>
<string name="media">Medien</string>
<string name="profile_about">Über</string>
<string name="button_follow">Folgen</string>
@@ -121,9 +122,20 @@
<string name="action_vote">Abstimmen</string>
<string name="tap_to_reveal">Zum Anzeigen tippen</string>
<string name="delete">Löschen</string>
<string name="delete_and_redraft">Löschen und neu erstellen</string>
<string name="confirm_delete_title">Beitrag löschen</string>
<string name="confirm_delete_and_redraft_title">Beitrag löschen und neu erstellen</string>
<string name="confirm_delete">Bist du dir sicher, dass du den Beitrag löschen möchtest?</string>
<string name="confirm_delete_and_redraft">Bist du dir sicher, dass du den Beitrag löschen und neu erstellen möchtest?</string>
<string name="deleting">Wird gelöscht…</string>
<string name="pin_post">An Profil anheften</string>
<string name="confirm_pin_post_title">Beitrag an Profil anheften</string>
<string name="confirm_pin_post">Möchtest du den Beitrag an dein Profil anheften?</string>
<string name="pinning">Wird angeheftet…</string>
<string name="unpin_post">Von Profil lösen</string>
<string name="confirm_unpin_post_title">Angehefteten Beitrag von Profil lösen</string>
<string name="confirm_unpin_post">Bist du dir sicher, dass du den angehefteten Beitrag von deinem Profil lösen möchtest?</string>
<string name="unpinning">Wird vom Profil gelöst…</string>
<string name="notification_channel_audio_player">Audiowiedergabe</string>
<string name="play">Abspielen</string>
<string name="pause">Pausieren</string>
@@ -317,7 +329,7 @@
<string name="manually_approves_followers">Genehmigt Folgende manuell</string>
<string name="current_account">Aktuelles Konto</string>
<string name="log_out_account">%s ausloggen</string>
<!-- translators: %,d is a valid placeholder, it formats the number with locale-dependent grouping separators -->
<!-- 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 Follower</item>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="shortcut_icon_background">@color/gray_700</color>
<color name="shortcut_icon_foreground">@color/primary_600</color>
</resources>

View File

@@ -92,5 +92,9 @@
<color name="highlight_over_light">#18000000</color>
<color name="favorite_selected">@color/warning_500</color>
<color name="bookmark_selected">@color/success_500</color>
<color name="boost_selected">@color/primary_500</color>
<color name="shortcut_icon_background">@color/gray_100</color>
<color name="shortcut_icon_foreground">@color/primary_700</color>
</resources>

View File

@@ -4,6 +4,7 @@
<item name="profile_posts" type="id"/>
<item name="profile_posts_with_replies" type="id"/>
<item name="profile_pinned_posts" type="id"/>
<item name="profile_media" type="id"/>
<item name="profile_about" type="id"/>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Mastodon</string>
<string name="app_name" translatable="false">Mastadon</string>
<string name="get_started">Get started</string>
<string name="log_in">Log in</string>
@@ -47,6 +47,7 @@
</plurals>
<string name="posts">Posts</string>
<string name="posts_and_replies">Posts and Replies</string>
<string name="pinned_posts">Pinned</string>
<string name="media">Media</string>
<string name="profile_about">About</string>
<string name="button_follow">Follow</string>
@@ -126,9 +127,20 @@
<string name="action_vote">Vote</string>
<string name="tap_to_reveal">Tap to reveal</string>
<string name="delete">Delete</string>
<string name="delete_and_redraft">Delete and re-draft</string>
<string name="confirm_delete_title">Delete Post</string>
<string name="confirm_delete_and_redraft_title">Delete and re-draft Post</string>
<string name="confirm_delete">Are you sure you want to delete this post?</string>
<string name="confirm_delete_and_redraft">Are you sure you want to delete and re-draft this post?</string>
<string name="deleting">Deleting…</string>
<string name="pin_post">Pin to profile</string>
<string name="confirm_pin_post_title">Pin post to profile</string>
<string name="confirm_pin_post">Do you want to pin this post to your profile?</string>
<string name="pinning">Pinning post…</string>
<string name="unpin_post">Unpin from profile</string>
<string name="confirm_unpin_post_title">Unpin post from profile</string>
<string name="confirm_unpin_post">Are you sure you want to unpin this post?</string>
<string name="unpinning">Unpinning post…</string>
<string name="notification_channel_audio_player">Audio playback</string>
<string name="play">Play</string>
<string name="pause">Pause</string>
@@ -280,6 +292,8 @@
<string name="button_reblog">Reblog</string>
<string name="button_favorite">Favorite</string>
<string name="button_share">Share</string>
<string name="button_bookmark">Bookmark</string>
<string name="bookmarks">Bookmarks</string>
<string name="media_no_description">Media without description</string>
<string name="add_media">Add media</string>
<string name="add_poll">Add a poll</string>
@@ -343,4 +357,5 @@
<item quantity="other">%,d reblogs</item>
</plurals>
<string name="timestamp_via_app">%1$s via %2$s</string>
<string name="time_now">now</string>
</resources>