Links and tags in statuses are now clickable and open suitable pages.

Mentions are also, incidentally, but still link to the account page for that user in the browser. This should be changed to an in-app account page when that's finished, but it's actually fairly suitable fallback behaviour for now.
This commit is contained in:
Vavassor 2017-01-26 19:34:32 -05:00
parent 83f8b4303c
commit dbb2663882
10 changed files with 185 additions and 19 deletions

View File

@ -34,6 +34,7 @@
android:windowSoftInputMode="stateVisible|adjustResize" />
<activity android:name=".ViewVideoActivity" />
<activity android:name=".ViewThreadActivity" />
<activity android:name=".ViewTagActivity" />
<service
android:name=".NotificationService"
android:description="@string/notification_service_description"

View File

@ -198,4 +198,8 @@ public class NotificationsFragment extends SFragment implements
Notification notification = adapter.getItem(position);
super.viewThread(notification.getStatus());
}
public void onViewTag(String tag) {
super.viewTag(tag);
}
}

View File

@ -244,4 +244,10 @@ public class SFragment extends Fragment {
intent.putExtra("id", status.getId());
startActivity(intent);
}
protected void viewTag(String tag) {
Intent intent = new Intent(getContext(), ViewTagActivity.class);
intent.putExtra("hashtag", tag);
startActivity(intent);
}
}

View File

@ -24,4 +24,5 @@ public interface StatusActionListener {
void onMore(View view, final int position);
void onViewMedia(String url, Status.MediaAttachment.Type type);
void onViewThread(int position);
void onViewTag(String tag);
}

View File

@ -18,7 +18,13 @@ package com.keylesspalace.tusky;
import android.content.Context;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.text.method.MovementMethod;
import android.text.style.ClickableSpan;
import android.text.style.URLSpan;
import android.text.util.Linkify;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
@ -28,7 +34,10 @@ import android.widget.TextView;
import com.android.volley.toolbox.ImageLoader;
import com.android.volley.toolbox.NetworkImageView;
import java.net.URL;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class StatusViewHolder extends RecyclerView.ViewHolder {
private View container;
@ -89,8 +98,32 @@ public class StatusViewHolder extends RecyclerView.ViewHolder {
username.setText(usernameText);
}
public void setContent(Spanned content) {
this.content.setText(content);
public void setContent(Spanned content, final StatusActionListener listener) {
// Redirect URLSpan's in the status content to the listener for viewing tag pages.
SpannableStringBuilder builder = new SpannableStringBuilder(content);
URLSpan[] urlSpans = content.getSpans(0, content.length(), URLSpan.class);
for (URLSpan span : urlSpans) {
int start = builder.getSpanStart(span);
int end = builder.getSpanEnd(span);
int flags = builder.getSpanFlags(span);
CharSequence tag = builder.subSequence(start, end);
if (tag.charAt(0) == '#') {
final String viewTag = tag.subSequence(1, tag.length()).toString();
ClickableSpan newSpan = new ClickableSpan() {
@Override
public void onClick(View widget) {
listener.onViewTag(viewTag);
}
};
builder.removeSpan(span);
builder.setSpan(newSpan, start, end, flags);
}
}
// Set the contents.
this.content.setText(builder);
// Make links clickable.
this.content.setLinksClickable(true);
this.content.setMovementMethod(LinkMovementMethod.getInstance());
}
public void setAvatar(String url) {
@ -203,6 +236,7 @@ public class StatusViewHolder extends RecyclerView.ViewHolder {
}
public void setupButtons(final StatusActionListener listener, final int position) {
replyButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@ -239,9 +273,8 @@ public class StatusViewHolder extends RecyclerView.ViewHolder {
setDisplayName(status.getDisplayName());
setUsername(status.getUsername());
setCreatedAt(status.getCreatedAt());
setContent(status.getContent());
setContent(status.getContent(), listener);
setAvatar(status.getAvatar());
setContent(status.getContent());
setReblogged(status.getReblogged());
setFavourited(status.getFavourited());
String rebloggedByUsername = status.getRebloggedByUsername();

View File

@ -47,12 +47,14 @@ public class TimelineFragment extends SFragment implements
HOME,
MENTIONS,
PUBLIC,
TAG,
}
private SwipeRefreshLayout swipeRefreshLayout;
private RecyclerView recyclerView;
private TimelineAdapter adapter;
private Kind kind;
private String hashtag;
private LinearLayoutManager layoutManager;
private EndlessOnScrollListener scrollListener;
private TabLayout.OnTabSelectedListener onTabSelectedListener;
@ -65,11 +67,24 @@ public class TimelineFragment extends SFragment implements
return fragment;
}
public static TimelineFragment newInstance(Kind kind, String hashtag) {
TimelineFragment fragment = new TimelineFragment();
Bundle arguments = new Bundle();
arguments.putString("kind", kind.name());
arguments.putString("hashtag", hashtag);
fragment.setArguments(arguments);
return fragment;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
kind = Kind.valueOf(getArguments().getString("kind"));
Bundle arguments = getArguments();
kind = Kind.valueOf(arguments.getString("kind"));
if (kind == Kind.TAG) {
hashtag = arguments.getString("hashtag");
}
View rootView = inflater.inflate(R.layout.fragment_timeline, container, false);
@ -103,20 +118,24 @@ public class TimelineFragment extends SFragment implements
adapter = new TimelineAdapter(this, this);
recyclerView.setAdapter(adapter);
TabLayout layout = (TabLayout) getActivity().findViewById(R.id.tab_layout);
onTabSelectedListener = new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {}
if (kind != Kind.TAG) {
TabLayout layout = (TabLayout) getActivity().findViewById(R.id.tab_layout);
onTabSelectedListener = new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
jumpToTop();
}
};
layout.addOnTabSelectedListener(onTabSelectedListener);
@Override
public void onTabReselected(TabLayout.Tab tab) {
jumpToTop();
}
};
layout.addOnTabSelectedListener(onTabSelectedListener);
}
sendFetchTimelineRequest();
@ -125,8 +144,10 @@ public class TimelineFragment extends SFragment implements
@Override
public void onDestroyView() {
TabLayout tabLayout = (TabLayout) getActivity().findViewById(R.id.tab_layout);
tabLayout.removeOnTabSelectedListener(onTabSelectedListener);
if (kind != Kind.TAG) {
TabLayout tabLayout = (TabLayout) getActivity().findViewById(R.id.tab_layout);
tabLayout.removeOnTabSelectedListener(onTabSelectedListener);
}
super.onDestroyView();
}
@ -151,6 +172,11 @@ public class TimelineFragment extends SFragment implements
endpoint = getString(R.string.endpoint_timelines_public);
break;
}
case TAG: {
assert(hashtag != null);
endpoint = String.format(getString(R.string.endpoint_timelines_tag), hashtag);
break;
}
}
String url = "https://" + domain + endpoint;
if (fromId != null) {
@ -250,4 +276,8 @@ public class TimelineFragment extends SFragment implements
public void onViewThread(int position) {
super.viewThread(adapter.getItem(position));
}
public void onViewTag(String tag) {
super.viewTag(tag);
}
}

View File

@ -0,0 +1,46 @@
/* Copyright 2017 Andrew Dawson
*
* This file is part of Tusky.
*
* Tusky is free software: you can redistribute it and/or modify it under the terms of the GNU
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky. If not, see
* <http://www.gnu.org/licenses/>. */
package com.keylesspalace.tusky;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
public class ViewTagActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_view_tag);
String hashtag = getIntent().getStringExtra("hashtag");
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
ActionBar bar = getSupportActionBar();
if (bar != null) {
bar.setTitle(String.format(getString(R.string.title_tag), hashtag));
}
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
Fragment fragment = TimelineFragment.newInstance(TimelineFragment.Kind.TAG, hashtag);
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();
}
}

View File

@ -140,4 +140,8 @@ public class ViewThreadFragment extends SFragment implements StatusActionListene
public void onViewThread(int position) {
super.viewThread(adapter.getItem(position));
}
public void onViewTag(String tag) {
super.viewTag(tag);
}
}

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/activity_view_thread"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.keylesspalace.tusky.ViewTagActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:elevation="4dp"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
<FrameLayout
android:id="@+id/overlay_fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
</FrameLayout>
</RelativeLayout>

View File

@ -5,6 +5,8 @@
<string name="oauth_redirect_host">oauth2redirect</string>
<string name="preferences_file_key">com.keylesspalace.tusky.PREFERENCES</string>
<string name="content_uri_format_tag">content://com.keylesspalace.tusky.viewtagactivity/%s</string>
<string name="endpoint_status">/api/v1/statuses</string>
<string name="endpoint_media">/api/v1/media</string>
<string name="endpoint_timelines_home">/api/v1/timelines/home</string>
@ -53,6 +55,7 @@
<string name="title_notifications">Notifications</string>
<string name="title_public">Public</string>
<string name="title_thread">Thread</string>
<string name="title_tag">#%s</string>
<string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s boosted</string>