Compare commits
29 Commits
v1.1.1-dev
...
v1.1.1+for
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad9518e87c | ||
|
|
1c16cfb09e | ||
|
|
d4a4b10017 | ||
|
|
74ae5bd04e | ||
|
|
9638cf079f | ||
|
|
a6d161c1b4 | ||
|
|
1136e40eb4 | ||
|
|
98de3a2984 | ||
|
|
080a320e12 | ||
|
|
b08415ca8f | ||
|
|
3639c69d36 | ||
|
|
37cefcaf6d | ||
|
|
558adc6936 | ||
|
|
31e3a8592f | ||
|
|
39655d5278 | ||
|
|
68d0862008 | ||
|
|
c9e13eefa5 | ||
|
|
349fbce5af | ||
|
|
95c66654aa | ||
|
|
a8407571a4 | ||
|
|
75538deb9b | ||
|
|
601eec4607 | ||
|
|
9b87d0bece | ||
|
|
cb25632691 | ||
|
|
63957250c5 | ||
|
|
bde2e398a8 | ||
|
|
8d443b2051 | ||
|
|
33d4b678ed | ||
|
|
3becad1468 |
@@ -9,8 +9,8 @@ android {
|
||||
applicationId "org.joinmastodon.android.sk"
|
||||
minSdk 23
|
||||
targetSdk 31
|
||||
versionCode 7
|
||||
versionName '1.1.1-dev+fork.7'
|
||||
versionCode 11
|
||||
versionName '1.1.1+fork.11'
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,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))
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -610,12 +610,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
|
||||
|
||||
@@ -515,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);
|
||||
}
|
||||
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()));
|
||||
|
||||
@@ -13,7 +13,6 @@ 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;
|
||||
|
||||
@@ -116,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))
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -205,7 +205,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
|
||||
|
||||
@Override
|
||||
public void onTabReselected(TabLayout.Tab tab){
|
||||
|
||||
scrollToTop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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(){
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)){
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -43,6 +43,7 @@ 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.HashtagTimelineFragment;
|
||||
@@ -108,7 +109,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);
|
||||
@@ -337,6 +340,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));
|
||||
}
|
||||
|
||||
@@ -361,6 +365,7 @@ public class UiUtils{
|
||||
@Override
|
||||
public void onSuccess(Status result) {
|
||||
resultCallback.accept(result);
|
||||
E.post(new StatusCountersUpdatedEvent(result));
|
||||
if (!result.pinned)
|
||||
E.post(new StatusUnpinnedEvent(status.id, accountID));
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
7
mastodon/src/main/res/drawable/ic_compose_foreground.xml
Normal file
7
mastodon/src/main/res/drawable/ic_compose_foreground.xml
Normal 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>
|
||||
@@ -46,6 +46,7 @@
|
||||
android:textAppearance="@style/m3_label_medium"
|
||||
android:textColor="?colorButtonText"
|
||||
android:gravity="end"
|
||||
android:singleLine="true"
|
||||
tools:text="1:23"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
5
mastodon/src/main/res/values-night/colors.xml
Normal file
5
mastodon/src/main/res/values-night/colors.xml
Normal 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>
|
||||
@@ -93,4 +93,7 @@
|
||||
|
||||
<color name="favorite_selected">@color/warning_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>
|
||||
@@ -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>
|
||||
@@ -352,4 +352,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>
|
||||
Reference in New Issue
Block a user