diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/InstanceInfoFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/InstanceInfoFragment.java new file mode 100644 index 000000000..e66c7a83f --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/InstanceInfoFragment.java @@ -0,0 +1,491 @@ +package org.joinmastodon.android.fragments; + +import android.app.Activity; +import android.content.res.Configuration; +import android.graphics.Outline; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.text.SpannableStringBuilder; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewOutlineProvider; +import android.view.WindowInsets; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.LinearLayoutManager; + +import org.joinmastodon.android.GlobalUserPreferences; +import org.joinmastodon.android.R; +import org.joinmastodon.android.api.requests.instance.GetInstance; +import org.joinmastodon.android.api.session.AccountSessionManager; +import org.joinmastodon.android.fragments.onboarding.InstanceRulesFragment; +import org.joinmastodon.android.model.Account; +import org.joinmastodon.android.model.AccountField; +import org.joinmastodon.android.model.Attachment; +import org.joinmastodon.android.model.Instance; +import org.joinmastodon.android.model.TimelineDefinition; +import org.joinmastodon.android.ui.BetterItemAnimator; +import org.joinmastodon.android.ui.SingleImagePhotoViewerListener; +import org.joinmastodon.android.ui.drawables.CoverOverlayGradientDrawable; +import org.joinmastodon.android.ui.photoviewer.PhotoViewer; +import org.joinmastodon.android.ui.text.CustomEmojiSpan; +import org.joinmastodon.android.ui.text.HtmlParser; +import org.joinmastodon.android.ui.utils.SimpleTextWatcher; +import org.joinmastodon.android.ui.utils.UiUtils; +import org.joinmastodon.android.ui.views.CoverImageView; +import org.joinmastodon.android.ui.views.LinkedTextView; +import org.parceler.Parcels; + +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import me.grishka.appkit.Nav; +import me.grishka.appkit.api.SimpleCallback; +import me.grishka.appkit.fragments.LoaderFragment; +import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter; +import me.grishka.appkit.imageloader.ImageLoaderViewHolder; +import me.grishka.appkit.imageloader.ListImageLoaderWrapper; +import me.grishka.appkit.imageloader.RecyclerViewDelegate; +import me.grishka.appkit.imageloader.ViewImageLoader; +import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; +import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; +import me.grishka.appkit.utils.BindableViewHolder; +import me.grishka.appkit.views.UsableRecyclerView; + +public class InstanceInfoFragment extends LoaderFragment { + + private Instance instance; + private CoverImageView cover; + private TextView uri, description; + + private Button timelineButton, pinButton, rulesButton, serversButton; + private final CoverOverlayGradientDrawable coverGradient=new CoverOverlayGradientDrawable(); + private float titleTransY; + private int statusBarHeight; + + private String accountID; + private Account account; + private String targetDomain; + private final ArrayList fields=new ArrayList<>(); + + private boolean refreshing; + private boolean updatedTimelines = false; + + private static final int MAX_FIELDS=4; + + // from ProfileAboutFragment + public UsableRecyclerView list; + private List metadataListData=Collections.emptyList(); + private MetadataAdapter adapter; + private ListImageLoaderWrapper imgLoader; + + public InstanceInfoFragment(){ + super(R.layout.loader_fragment_overlay_toolbar); + } + + @Override + public void onCreate(Bundle savedInstanceState){ + super.onCreate(savedInstanceState); + if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N) + setRetainInstance(true); + + accountID=getArguments().getString("account"); + account= AccountSessionManager.getInstance().getAccount(accountID).self; + targetDomain=getArguments().getString("instanceDomain"); + loadData(); + } + + @Override + public void onAttach(Activity activity){ + super.onAttach(activity); + setHasOptionsMenu(true); + } + + @Override + public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){ + View content=inflater.inflate(R.layout.fragment_instance_info, container, false); + + cover=content.findViewById(R.id.cover); + uri =content.findViewById(R.id.uri); + description=content.findViewById(R.id.description); + timelineButton=content.findViewById(R.id.timeline_btn); + pinButton=content.findViewById(R.id.timeline_pin_btn); + rulesButton=content.findViewById(R.id.rules_btn); + serversButton=content.findViewById(R.id.servers_btn); + list=content.findViewById(R.id.metadata); + + cover.setForeground(coverGradient); + cover.setOutlineProvider(new ViewOutlineProvider(){ + @Override + public void getOutline(View view, Outline outline){ + outline.setEmpty(); + } + }); + + timelineButton.setOnClickListener(this::onTimelineButtonClick); + pinButton.setOnClickListener(this::onPinButtonClick); + rulesButton.setOnClickListener(this::onRulesButtonClick); + serversButton.setOnClickListener(this::onSeversButtonClick); + cover.setOnClickListener(this::onCoverClick); + + if(loaded){ + bindHeaderView(); + dataLoaded(); + } + + // from ProfileAboutFragment + list.setItemAnimator(new BetterItemAnimator()); + list.setDrawSelectorOnTop(true); + list.setLayoutManager(new LinearLayoutManager(getActivity())); + imgLoader=new ListImageLoaderWrapper(getActivity(), list, new RecyclerViewDelegate(list), null); + list.setAdapter(adapter=new MetadataAdapter()); + list.setClipToPadding(false); + + return content; + } + + @Override + protected void doLoadData(){ + currentRequest=new GetInstance() + .setCallback(new SimpleCallback<>(this){ + @Override + public void onSuccess(Instance result){ + if (getActivity() == null) return; + instance = result; + bindHeaderView(); + dataLoaded(); + + } + }) + //hack to get instance url for local and remote accounts + .execNoAuth(targetDomain); + } + + @Override + public void onRefresh(){ + if(refreshing) + return; + refreshing=true; + doLoadData(); + } + + @Override + public void dataLoaded(){ + if(getActivity()==null) + return; + setFields(fields); + super.dataLoaded(); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState){ + super.onViewCreated(view, savedInstanceState); + updateToolbar(); + // To avoid the callback triggering on first layout with position=0 before anything is instantiated + + titleTransY=getToolbar().getLayoutParams().height; + if(toolbarTitleView!=null){ + toolbarTitleView.setTranslationY(titleTransY); + toolbarSubtitleView.setTranslationY(titleTransY); + } + } + + @Override + public void onDestroyView(){ + super.onDestroyView(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (updatedTimelines) UiUtils.restartApp(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig){ + super.onConfigurationChanged(newConfig); + updateToolbar(); + } + + @Override + public void onApplyWindowInsets(WindowInsets insets){ + statusBarHeight=insets.getSystemWindowInsetTop(); + if(contentView!=null){ + ((ViewGroup.MarginLayoutParams) getToolbar().getLayoutParams()).topMargin=statusBarHeight; + } + super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom())); + } + + + + private void bindHeaderView(){ + ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(instance.thumbnail, 1000, 1000)); + uri.setText(instance.title); + setTitle(instance.title); + + CharSequence parsedDescription = HtmlParser.parse(TextUtils.isEmpty(instance.description) ? instance.shortDescription : instance.description, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID); + description.setText(parsedDescription); + + fields.clear(); + + + if (instance.contactAccount != null) { + AccountField admin = new AccountField(); + admin.parsedName=admin.name= "Administered by"; + admin.parsedValue=buildLinkText(instance.contactAccount.url, instance.contactAccount.getDisplayUsername() + "@" + instance.uri); + fields.add(admin); + } + + if (instance.email != null) { + AccountField contact = new AccountField(); + contact.parsedName = contact.name = "Contact"; + contact.parsedValue=buildLinkText("mailto:" + instance.email, instance.email); + fields.add(contact); + } + + if (instance.stats != null) { + AccountField activeUsers = new AccountField(); + activeUsers.parsedName = activeUsers.name = "users"; + activeUsers.parsedValue= NumberFormat.getInstance().format(instance.stats.userCount); + fields.add(activeUsers); + } + + + setFields(fields); + } + + private SpannableStringBuilder buildLinkText(String link, String text) { + String value = "" + text + ""; + return HtmlParser.parse(value, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID); + } + + private void updateToolbar(){ + getToolbar().setBackgroundColor(0); + if(toolbarTitleView!=null){ + toolbarTitleView.setTranslationY(titleTransY); + toolbarSubtitleView.setTranslationY(titleTransY); + } + getToolbar().setNavigationContentDescription(R.string.back); + } + + @Override + public boolean wantsLightStatusBar(){ + return false; + } + + @Override + protected int getToolbarResource(){ + return R.layout.profile_toolbar; + } + + private void onTimelineButtonClick(View view) { + Bundle args=new Bundle(); + args.putString("account", accountID); + args.putString("domain", instance.uri); + Nav.go(getActivity(), CustomLocalTimelineFragment.class, args); + } + + private void onPinButtonClick(View view) { + List timelines = GlobalUserPreferences.pinnedTimelines.get(accountID); + if (timelines == null) { + timelines = List.of(TimelineDefinition.HOME_TIMELINE); + } + timelines.add(TimelineDefinition.ofCustomLocalTimeline(instance.uri)); + GlobalUserPreferences.pinnedTimelines.put(accountID, timelines); + GlobalUserPreferences.save(); + updatedTimelines = true; + } + + private void onRulesButtonClick(View view) { + Bundle args=new Bundle(); + args.putParcelable("instance", Parcels.wrap(instance)); + Nav.go(getActivity(), InstanceRulesFragment.class, args); + } + private void onSeversButtonClick(View view) { + } + + + private void onCoverClick(View v){ + Drawable drawable=cover.getDrawable(); + if(drawable==null || drawable instanceof ColorDrawable) + return; + new PhotoViewer(getActivity(), createFakeAttachments(instance.thumbnail, drawable), 0, + new SingleImagePhotoViewerListener(cover, cover, null, this, () -> { + }, () -> drawable, null, null)); + } + + private List createFakeAttachments(String url, Drawable drawable){ + Attachment att=new Attachment(); + att.type=Attachment.Type.IMAGE; + att.url=url; + att.meta=new Attachment.Metadata(); + att.meta.width=drawable.getIntrinsicWidth(); + att.meta.height=drawable.getIntrinsicHeight(); + return Collections.singletonList(att); + } + + + + // from ProfileAboutFragment + public void setFields(ArrayList fields){ + metadataListData=fields; + if (adapter != null) adapter.notifyDataSetChanged(); + } + + private class MetadataAdapter extends UsableRecyclerView.Adapter implements ImageLoaderRecyclerAdapter { + public MetadataAdapter(){ + super(imgLoader); + } + + @NonNull + @Override + public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ + return switch(viewType){ + case 0 -> new AboutViewHolder(); + case 1 -> new EditableAboutViewHolder(); + case 2 -> new AddRowViewHolder(); + default -> throw new IllegalStateException("Unexpected value: "+viewType); + }; + } + + @Override + public void onBindViewHolder(BaseViewHolder holder, int position){ + if(position { + public BaseViewHolder(int layout){ + super(getActivity(), layout, list); + } + } + + private class AboutViewHolder extends BaseViewHolder implements ImageLoaderViewHolder { + private TextView title; + private LinkedTextView value; + + public AboutViewHolder(){ + super(R.layout.item_profile_about); + title=findViewById(R.id.title); + value=findViewById(R.id.value); + } + + @Override + public void onBind(AccountField item){ + title.setText(item.parsedName); + value.setText(item.parsedValue); + if(item.verifiedAt!=null){ + int textColor=UiUtils.isDarkTheme() ? 0xFF89bb9c : 0xFF5b8e63; + value.setTextColor(textColor); + value.setLinkTextColor(textColor); + Drawable check=getResources().getDrawable(R.drawable.ic_fluent_checkmark_24_regular, getActivity().getTheme()).mutate(); + check.setTint(textColor); + value.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, check, null); + }else{ + value.setTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary)); + value.setLinkTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.colorAccent)); + value.setCompoundDrawables(null, null, null, null); + } + } + + @Override + public void setImage(int index, Drawable image){ + CustomEmojiSpan span=index>=item.nameEmojis.length ? item.valueEmojis[index-item.nameEmojis.length] : item.nameEmojis[index]; + span.setDrawable(image); + title.invalidate(); + value.invalidate(); + } + + @Override + public void clearImage(int index){ + setImage(index, null); + } + } + + private class EditableAboutViewHolder extends BaseViewHolder { + private EditText title; + private EditText value; + + public EditableAboutViewHolder(){ + super(R.layout.item_profile_about_editable); + title=findViewById(R.id.title); + value=findViewById(R.id.value); + findViewById(R.id.dragger_thingy).setOnLongClickListener(v-> true); + title.addTextChangedListener(new SimpleTextWatcher(e->item.name=e.toString())); + value.addTextChangedListener(new SimpleTextWatcher(e->item.value=e.toString())); + findViewById(R.id.remove_row_btn).setOnClickListener(this::onRemoveRowClick); + } + + @Override + public void onBind(AccountField item){ + title.setText(item.name); + value.setText(item.value); + } + + private void onRemoveRowClick(View v){ + int pos=getAbsoluteAdapterPosition(); + metadataListData.remove(pos); + adapter.notifyItemRemoved(pos); + for(int i=0;i { + Bundle args=new Bundle(); + args.putString("account", accountID); + args.putString("instanceDomain", Uri.parse(account.url).getHost()); + Nav.go(getActivity(), InstanceInfoFragment.class, args); + }); + username.setOnLongClickListener(v->{ String usernameString=account.acct; if(!usernameString.contains("@")){ diff --git a/mastodon/src/main/res/layout/fragment_instance_info.xml b/mastodon/src/main/res/layout/fragment_instance_info.xml new file mode 100644 index 000000000..bae16900a --- /dev/null +++ b/mastodon/src/main/res/layout/fragment_instance_info.xml @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + + + + + + + + + +