mirror of https://github.com/tuskyapp/Tusky.git
31 changed files with 1256 additions and 548 deletions
@ -0,0 +1,5 @@
|
||||
package com.keylesspalace.tusky; |
||||
|
||||
public interface AdapterItemRemover { |
||||
void removeItem(int position); |
||||
} |
||||
@ -0,0 +1,50 @@
|
||||
/* 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; |
||||
|
||||
public class DateUtils { |
||||
/* This is a rough duplicate of android.text.format.DateUtils.getRelativeTimeSpanString, |
||||
* but even with the FORMAT_ABBREV_RELATIVE flag it wasn't abbreviating enough. */ |
||||
public static String getRelativeTimeSpanString(long then, long now) { |
||||
final long MINUTE = 60; |
||||
final long HOUR = 60 * MINUTE; |
||||
final long DAY = 24 * HOUR; |
||||
final long YEAR = 365 * DAY; |
||||
long span = (now - then) / 1000; |
||||
String prefix = ""; |
||||
if (span < 0) { |
||||
prefix = "in "; |
||||
span = -span; |
||||
} |
||||
String unit; |
||||
if (span < MINUTE) { |
||||
unit = "s"; |
||||
} else if (span < HOUR) { |
||||
span /= MINUTE; |
||||
unit = "m"; |
||||
} else if (span < DAY) { |
||||
span /= HOUR; |
||||
unit = "h"; |
||||
} else if (span < YEAR) { |
||||
span /= DAY; |
||||
unit = "d"; |
||||
} else { |
||||
span /= YEAR; |
||||
unit = "y"; |
||||
} |
||||
return prefix + span + unit; |
||||
} |
||||
} |
||||
@ -0,0 +1,55 @@
|
||||
/* 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.support.v7.widget.RecyclerView; |
||||
import android.view.View; |
||||
import android.widget.Button; |
||||
import android.widget.LinearLayout; |
||||
import android.widget.ProgressBar; |
||||
|
||||
public class FooterViewHolder extends RecyclerView.ViewHolder { |
||||
private LinearLayout retryBar; |
||||
private Button retry; |
||||
private ProgressBar progressBar; |
||||
|
||||
public FooterViewHolder(View itemView) { |
||||
super(itemView); |
||||
retryBar = (LinearLayout) itemView.findViewById(R.id.footer_retry_bar); |
||||
retry = (Button) itemView.findViewById(R.id.footer_retry_button); |
||||
progressBar = (ProgressBar) itemView.findViewById(R.id.footer_progress_bar); |
||||
progressBar.setIndeterminate(true); |
||||
} |
||||
|
||||
public void setupButton(final FooterActionListener listener) { |
||||
retry.setOnClickListener(new View.OnClickListener() { |
||||
@Override |
||||
public void onClick(View v) { |
||||
listener.onLoadMore(); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
public void showRetry(boolean show) { |
||||
if (!show) { |
||||
retryBar.setVisibility(View.GONE); |
||||
progressBar.setVisibility(View.VISIBLE); |
||||
} else { |
||||
retryBar.setVisibility(View.VISIBLE); |
||||
progressBar.setVisibility(View.GONE); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,247 @@
|
||||
/* 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.content.Context; |
||||
import android.content.Intent; |
||||
import android.content.SharedPreferences; |
||||
import android.os.Bundle; |
||||
import android.support.annotation.Nullable; |
||||
import android.support.v4.app.Fragment; |
||||
import android.support.v4.app.FragmentManager; |
||||
import android.support.v7.widget.PopupMenu; |
||||
import android.support.v7.widget.RecyclerView; |
||||
import android.view.MenuItem; |
||||
import android.view.View; |
||||
|
||||
import com.android.volley.AuthFailureError; |
||||
import com.android.volley.Request; |
||||
import com.android.volley.Response; |
||||
import com.android.volley.VolleyError; |
||||
import com.android.volley.toolbox.JsonObjectRequest; |
||||
|
||||
import org.json.JSONException; |
||||
import org.json.JSONObject; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
/* Note from Andrew on Jan. 22, 2017: This class is a design problem for me, so I left it with an |
||||
* awkward name. TimelineFragment and NotificationFragment have significant overlap but the nature |
||||
* of that is complicated by how they're coupled with Status and Notification and the corresponding |
||||
* adapters. I feel like the profile pages and thread viewer, which I haven't made yet, will also |
||||
* overlap functionality. So, I'm momentarily leaving it and hopefully working on those will clear |
||||
* up what needs to be where. */ |
||||
public class SFragment extends Fragment { |
||||
protected String domain; |
||||
protected String accessToken; |
||||
protected String loggedInAccountId; |
||||
protected String loggedInUsername; |
||||
|
||||
@Override |
||||
public void onCreate(@Nullable Bundle savedInstanceState) { |
||||
super.onCreate(savedInstanceState); |
||||
|
||||
SharedPreferences preferences = getContext().getSharedPreferences( |
||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE); |
||||
domain = preferences.getString("domain", null); |
||||
accessToken = preferences.getString("accessToken", null); |
||||
assert(domain != null); |
||||
assert(accessToken != null); |
||||
|
||||
sendUserInfoRequest(); |
||||
} |
||||
|
||||
protected void sendRequest( |
||||
int method, String endpoint, JSONObject parameters, |
||||
@Nullable Response.Listener<JSONObject> responseListener) { |
||||
if (responseListener == null) { |
||||
// Use a dummy listener if one wasn't specified so the request can be constructed.
|
||||
responseListener = new Response.Listener<JSONObject>() { |
||||
@Override |
||||
public void onResponse(JSONObject response) {} |
||||
}; |
||||
} |
||||
String url = "https://" + domain + endpoint; |
||||
JsonObjectRequest request = new JsonObjectRequest( |
||||
method, url, parameters, responseListener, |
||||
new Response.ErrorListener() { |
||||
@Override |
||||
public void onErrorResponse(VolleyError error) { |
||||
System.err.println(error.getMessage()); |
||||
} |
||||
}) { |
||||
@Override |
||||
public Map<String, String> getHeaders() throws AuthFailureError { |
||||
Map<String, String> headers = new HashMap<>(); |
||||
headers.put("Authorization", "Bearer " + accessToken); |
||||
return headers; |
||||
} |
||||
}; |
||||
VolleySingleton.getInstance(getContext()).addToRequestQueue(request); |
||||
} |
||||
|
||||
protected void postRequest(String endpoint) { |
||||
sendRequest(Request.Method.POST, endpoint, null, null); |
||||
} |
||||
|
||||
private void sendUserInfoRequest() { |
||||
sendRequest(Request.Method.GET, getString(R.string.endpoint_verify_credentials), null, |
||||
new Response.Listener<JSONObject>() { |
||||
@Override |
||||
public void onResponse(JSONObject response) { |
||||
try { |
||||
loggedInAccountId = response.getString("id"); |
||||
loggedInUsername = response.getString("acct"); |
||||
} catch (JSONException e) { |
||||
//TODO: Help
|
||||
assert(false); |
||||
} |
||||
} |
||||
}); |
||||
} |
||||
|
||||
protected void reply(Status status) { |
||||
String inReplyToId = status.getId(); |
||||
Status.Mention[] mentions = status.getMentions(); |
||||
List<String> mentionedUsernames = new ArrayList<>(); |
||||
for (int i = 0; i < mentions.length; i++) { |
||||
mentionedUsernames.add(mentions[i].getUsername()); |
||||
} |
||||
mentionedUsernames.add(status.getUsername()); |
||||
mentionedUsernames.remove(loggedInUsername); |
||||
Intent intent = new Intent(getContext(), ComposeActivity.class); |
||||
intent.putExtra("in_reply_to_id", inReplyToId); |
||||
intent.putExtra("mentioned_usernames", mentionedUsernames.toArray(new String[0])); |
||||
startActivity(intent); |
||||
} |
||||
|
||||
protected void reblog(final Status status, final boolean reblog, |
||||
final RecyclerView.Adapter adapter, final int position) { |
||||
String id = status.getId(); |
||||
String endpoint; |
||||
if (reblog) { |
||||
endpoint = String.format(getString(R.string.endpoint_reblog), id); |
||||
} else { |
||||
endpoint = String.format(getString(R.string.endpoint_unreblog), id); |
||||
} |
||||
sendRequest(Request.Method.POST, endpoint, null, |
||||
new Response.Listener<JSONObject>() { |
||||
@Override |
||||
public void onResponse(JSONObject response) { |
||||
status.setReblogged(reblog); |
||||
adapter.notifyItemChanged(position); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
protected void favourite(final Status status, final boolean favourite, |
||||
final RecyclerView.Adapter adapter, final int position) { |
||||
String id = status.getId(); |
||||
String endpoint; |
||||
if (favourite) { |
||||
endpoint = String.format(getString(R.string.endpoint_favourite), id); |
||||
} else { |
||||
endpoint = String.format(getString(R.string.endpoint_unfavourite), id); |
||||
} |
||||
sendRequest(Request.Method.POST, endpoint, null, new Response.Listener<JSONObject>() { |
||||
@Override |
||||
public void onResponse(JSONObject response) { |
||||
status.setFavourited(favourite); |
||||
adapter.notifyItemChanged(position); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
private void follow(String id) { |
||||
String endpoint = String.format(getString(R.string.endpoint_follow), id); |
||||
postRequest(endpoint); |
||||
} |
||||
|
||||
private void block(String id) { |
||||
String endpoint = String.format(getString(R.string.endpoint_block), id); |
||||
postRequest(endpoint); |
||||
} |
||||
|
||||
private void delete(String id) { |
||||
String endpoint = String.format(getString(R.string.endpoint_delete), id); |
||||
sendRequest(Request.Method.DELETE, endpoint, null, null); |
||||
} |
||||
|
||||
protected void more(Status status, View view, final AdapterItemRemover adapter, |
||||
final int position) { |
||||
final String id = status.getId(); |
||||
final String accountId = status.getAccountId(); |
||||
PopupMenu popup = new PopupMenu(getContext(), view); |
||||
// Give a different menu depending on whether this is the user's own toot or not.
|
||||
if (loggedInAccountId == null || !loggedInAccountId.equals(accountId)) { |
||||
popup.inflate(R.menu.status_more); |
||||
} else { |
||||
popup.inflate(R.menu.status_more_for_user); |
||||
} |
||||
popup.setOnMenuItemClickListener( |
||||
new PopupMenu.OnMenuItemClickListener() { |
||||
@Override |
||||
public boolean onMenuItemClick(MenuItem item) { |
||||
switch (item.getItemId()) { |
||||
case R.id.status_follow: { |
||||
follow(accountId); |
||||
return true; |
||||
} |
||||
case R.id.status_block: { |
||||
block(accountId); |
||||
return true; |
||||
} |
||||
case R.id.status_delete: { |
||||
delete(id); |
||||
adapter.removeItem(position); |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
}); |
||||
popup.show(); |
||||
} |
||||
|
||||
protected void viewMedia(String url, Status.MediaAttachment.Type type) { |
||||
switch (type) { |
||||
case IMAGE: { |
||||
Fragment newFragment = ViewMediaFragment.newInstance(url); |
||||
FragmentManager manager = getFragmentManager(); |
||||
manager.beginTransaction() |
||||
.add(R.id.overlay_fragment_container, newFragment) |
||||
.addToBackStack(null) |
||||
.commit(); |
||||
break; |
||||
} |
||||
case VIDEO: { |
||||
Intent intent = new Intent(getContext(), ViewVideoActivity.class); |
||||
intent.putExtra("url", url); |
||||
startActivity(intent); |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
protected void viewThread(Status status) { |
||||
Intent intent = new Intent(getContext(), ViewThreadActivity.class); |
||||
intent.putExtra("id", status.getId()); |
||||
startActivity(intent); |
||||
} |
||||
} |
||||
@ -0,0 +1,266 @@
|
||||
/* 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.content.Context; |
||||
import android.support.annotation.Nullable; |
||||
import android.support.v7.widget.RecyclerView; |
||||
import android.text.Spanned; |
||||
import android.view.View; |
||||
import android.view.ViewGroup; |
||||
import android.widget.ImageButton; |
||||
import android.widget.ImageView; |
||||
import android.widget.TextView; |
||||
|
||||
import com.android.volley.toolbox.ImageLoader; |
||||
import com.android.volley.toolbox.NetworkImageView; |
||||
|
||||
import java.util.Date; |
||||
|
||||
public class StatusViewHolder extends RecyclerView.ViewHolder { |
||||
private View container; |
||||
private TextView displayName; |
||||
private TextView username; |
||||
private TextView sinceCreated; |
||||
private TextView content; |
||||
private NetworkImageView avatar; |
||||
private ImageView boostedIcon; |
||||
private TextView boostedByUsername; |
||||
private ImageButton replyButton; |
||||
private ImageButton reblogButton; |
||||
private ImageButton favouriteButton; |
||||
private ImageButton moreButton; |
||||
private boolean favourited; |
||||
private boolean reblogged; |
||||
private NetworkImageView mediaPreview0; |
||||
private NetworkImageView mediaPreview1; |
||||
private NetworkImageView mediaPreview2; |
||||
private NetworkImageView mediaPreview3; |
||||
private View sensitiveMediaWarning; |
||||
|
||||
public StatusViewHolder(View itemView) { |
||||
super(itemView); |
||||
container = itemView.findViewById(R.id.status_container); |
||||
displayName = (TextView) itemView.findViewById(R.id.status_display_name); |
||||
username = (TextView) itemView.findViewById(R.id.status_username); |
||||
sinceCreated = (TextView) itemView.findViewById(R.id.status_since_created); |
||||
content = (TextView) itemView.findViewById(R.id.status_content); |
||||
avatar = (NetworkImageView) itemView.findViewById(R.id.status_avatar); |
||||
boostedIcon = (ImageView) itemView.findViewById(R.id.status_boosted_icon); |
||||
boostedByUsername = (TextView) itemView.findViewById(R.id.status_boosted); |
||||
replyButton = (ImageButton) itemView.findViewById(R.id.status_reply); |
||||
reblogButton = (ImageButton) itemView.findViewById(R.id.status_reblog); |
||||
favouriteButton = (ImageButton) itemView.findViewById(R.id.status_favourite); |
||||
moreButton = (ImageButton) itemView.findViewById(R.id.status_more); |
||||
reblogged = false; |
||||
favourited = false; |
||||
mediaPreview0 = (NetworkImageView) itemView.findViewById(R.id.status_media_preview_0); |
||||
mediaPreview1 = (NetworkImageView) itemView.findViewById(R.id.status_media_preview_1); |
||||
mediaPreview2 = (NetworkImageView) itemView.findViewById(R.id.status_media_preview_2); |
||||
mediaPreview3 = (NetworkImageView) itemView.findViewById(R.id.status_media_preview_3); |
||||
mediaPreview0.setDefaultImageResId(R.drawable.media_preview_unloaded); |
||||
mediaPreview1.setDefaultImageResId(R.drawable.media_preview_unloaded); |
||||
mediaPreview2.setDefaultImageResId(R.drawable.media_preview_unloaded); |
||||
mediaPreview3.setDefaultImageResId(R.drawable.media_preview_unloaded); |
||||
sensitiveMediaWarning = itemView.findViewById(R.id.status_sensitive_media_warning); |
||||
} |
||||
|
||||
public void setDisplayName(String name) { |
||||
displayName.setText(name); |
||||
} |
||||
|
||||
public void setUsername(String name) { |
||||
Context context = username.getContext(); |
||||
String format = context.getString(R.string.status_username_format); |
||||
String usernameText = String.format(format, name); |
||||
username.setText(usernameText); |
||||
} |
||||
|
||||
public void setContent(Spanned content) { |
||||
this.content.setText(content); |
||||
} |
||||
|
||||
public void setAvatar(String url) { |
||||
Context context = avatar.getContext(); |
||||
ImageLoader imageLoader = VolleySingleton.getInstance(context).getImageLoader(); |
||||
avatar.setImageUrl(url, imageLoader); |
||||
avatar.setDefaultImageResId(R.drawable.avatar_default); |
||||
avatar.setErrorImageResId(R.drawable.avatar_error); |
||||
} |
||||
|
||||
public void setCreatedAt(@Nullable Date createdAt) { |
||||
String readout; |
||||
if (createdAt != null) { |
||||
long then = createdAt.getTime(); |
||||
long now = new Date().getTime(); |
||||
readout = DateUtils.getRelativeTimeSpanString(then, now); |
||||
} else { |
||||
readout = "?m"; // unknown minutes~
|
||||
} |
||||
sinceCreated.setText(readout); |
||||
} |
||||
|
||||
public void setRebloggedByUsername(String name) { |
||||
Context context = boostedByUsername.getContext(); |
||||
String format = context.getString(R.string.status_boosted_format); |
||||
String boostedText = String.format(format, name); |
||||
boostedByUsername.setText(boostedText); |
||||
boostedIcon.setVisibility(View.VISIBLE); |
||||
boostedByUsername.setVisibility(View.VISIBLE); |
||||
} |
||||
|
||||
public void hideRebloggedByUsername() { |
||||
boostedIcon.setVisibility(View.GONE); |
||||
boostedByUsername.setVisibility(View.GONE); |
||||
} |
||||
|
||||
public void setReblogged(boolean reblogged) { |
||||
this.reblogged = reblogged; |
||||
if (!reblogged) { |
||||
reblogButton.setImageResource(R.drawable.ic_reblog_off); |
||||
} else { |
||||
reblogButton.setImageResource(R.drawable.ic_reblog_on); |
||||
} |
||||
} |
||||
|
||||
public void disableReblogging() { |
||||
reblogButton.setEnabled(false); |
||||
reblogButton.setImageResource(R.drawable.ic_reblog_disabled); |
||||
} |
||||
|
||||
public void setFavourited(boolean favourited) { |
||||
this.favourited = favourited; |
||||
if (!favourited) { |
||||
favouriteButton.setImageResource(R.drawable.ic_favourite_off); |
||||
} else { |
||||
favouriteButton.setImageResource(R.drawable.ic_favourite_on); |
||||
} |
||||
} |
||||
|
||||
public void setMediaPreviews(final Status.MediaAttachment[] attachments, |
||||
boolean sensitive, final StatusActionListener listener) { |
||||
final NetworkImageView[] previews = { |
||||
mediaPreview0, |
||||
mediaPreview1, |
||||
mediaPreview2, |
||||
mediaPreview3 |
||||
}; |
||||
Context context = mediaPreview0.getContext(); |
||||
ImageLoader imageLoader = VolleySingleton.getInstance(context).getImageLoader(); |
||||
final int n = Math.min(attachments.length, Status.MAX_MEDIA_ATTACHMENTS); |
||||
for (int i = 0; i < n; i++) { |
||||
String previewUrl = attachments[i].getPreviewUrl(); |
||||
previews[i].setImageUrl(previewUrl, imageLoader); |
||||
if (!sensitive) { |
||||
previews[i].setVisibility(View.VISIBLE); |
||||
} else { |
||||
previews[i].setVisibility(View.GONE); |
||||
} |
||||
final String url = attachments[i].getUrl(); |
||||
final Status.MediaAttachment.Type type = attachments[i].getType(); |
||||
previews[i].setOnClickListener(new View.OnClickListener() { |
||||
@Override |
||||
public void onClick(View v) { |
||||
listener.onViewMedia(url, type); |
||||
} |
||||
}); |
||||
} |
||||
if (sensitive) { |
||||
sensitiveMediaWarning.setVisibility(View.VISIBLE); |
||||
sensitiveMediaWarning.setOnClickListener(new View.OnClickListener() { |
||||
@Override |
||||
public void onClick(View v) { |
||||
v.setVisibility(View.GONE); |
||||
for (int i = 0; i < n; i++) { |
||||
previews[i].setVisibility(View.VISIBLE); |
||||
} |
||||
v.setOnClickListener(null); |
||||
} |
||||
}); |
||||
} |
||||
// Hide any of the placeholder previews beyond the ones set.
|
||||
for (int i = n; i < Status.MAX_MEDIA_ATTACHMENTS; i++) { |
||||
previews[i].setImageUrl(null, imageLoader); |
||||
previews[i].setVisibility(View.GONE); |
||||
} |
||||
} |
||||
|
||||
public void hideSensitiveMediaWarning() { |
||||
sensitiveMediaWarning.setVisibility(View.GONE); |
||||
} |
||||
|
||||
public void setupButtons(final StatusActionListener listener, final int position) { |
||||
replyButton.setOnClickListener(new View.OnClickListener() { |
||||
@Override |
||||
public void onClick(View v) { |
||||
listener.onReply(position); |
||||
} |
||||
}); |
||||
reblogButton.setOnClickListener(new View.OnClickListener() { |
||||
@Override |
||||
public void onClick(View v) { |
||||
listener.onReblog(!reblogged, position); |
||||
} |
||||
}); |
||||
favouriteButton.setOnClickListener(new View.OnClickListener() { |
||||
@Override |
||||
public void onClick(View v) { |
||||
listener.onFavourite(!favourited, position); |
||||
} |
||||
}); |
||||
moreButton.setOnClickListener(new View.OnClickListener() { |
||||
@Override |
||||
public void onClick(View v) { |
||||
listener.onMore(v, position); |
||||
} |
||||
}); |
||||
container.setOnClickListener(new View.OnClickListener() { |
||||
@Override |
||||
public void onClick(View v) { |
||||
listener.onViewThread(position); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
public void setupWithStatus(Status status, StatusActionListener listener, int position) { |
||||
setDisplayName(status.getDisplayName()); |
||||
setUsername(status.getUsername()); |
||||
setCreatedAt(status.getCreatedAt()); |
||||
setContent(status.getContent()); |
||||
setAvatar(status.getAvatar()); |
||||
setContent(status.getContent()); |
||||
setReblogged(status.getReblogged()); |
||||
setFavourited(status.getFavourited()); |
||||
String rebloggedByUsername = status.getRebloggedByUsername(); |
||||
if (rebloggedByUsername == null) { |
||||
hideRebloggedByUsername(); |
||||
} else { |
||||
setRebloggedByUsername(rebloggedByUsername); |
||||
} |
||||
Status.MediaAttachment[] attachments = status.getAttachments(); |
||||
boolean sensitive = status.getSensitive(); |
||||
setMediaPreviews(attachments, sensitive, listener); |
||||
/* A status without attachments is sometimes still marked sensitive, so it's necessary to |
||||
* check both whether there are any attachments and if it's marked sensitive. */ |
||||
if (!sensitive || attachments.length == 0) { |
||||
hideSensitiveMediaWarning(); |
||||
} |
||||
setupButtons(listener, position); |
||||
if (status.getVisibility() == Status.Visibility.PRIVATE) { |
||||
disableReblogging(); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,83 @@
|
||||
/* 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.support.v7.widget.RecyclerView; |
||||
import android.view.LayoutInflater; |
||||
import android.view.View; |
||||
import android.view.ViewGroup; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
public class ThreadAdapter extends RecyclerView.Adapter implements AdapterItemRemover { |
||||
private List<Status> statuses; |
||||
private StatusActionListener statusActionListener; |
||||
private int statusIndex; |
||||
|
||||
public ThreadAdapter(StatusActionListener listener) { |
||||
this.statusActionListener = listener; |
||||
this.statuses = new ArrayList<>(); |
||||
this.statusIndex = 0; |
||||
} |
||||
|
||||
@Override |
||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { |
||||
View view = LayoutInflater.from(parent.getContext()) |
||||
.inflate(R.layout.item_status, parent, false); |
||||
return new StatusViewHolder(view); |
||||
} |
||||
|
||||
@Override |
||||
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { |
||||
StatusViewHolder holder = (StatusViewHolder) viewHolder; |
||||
Status status = statuses.get(position); |
||||
holder.setupWithStatus(status, statusActionListener, position); |
||||
} |
||||
|
||||
@Override |
||||
public int getItemCount() { |
||||
return statuses.size(); |
||||
} |
||||
|
||||
public Status getItem(int position) { |
||||
return statuses.get(position); |
||||
} |
||||
|
||||
public void removeItem(int position) { |
||||
statuses.remove(position); |
||||
notifyItemRemoved(position); |
||||
} |
||||
|
||||
public int insertStatus(Status status) { |
||||
int i = statusIndex; |
||||
statuses.add(i, status); |
||||
notifyItemInserted(i); |
||||
return i; |
||||
} |
||||
|
||||
public void addAncestors(List<Status> ancestors) { |
||||
statusIndex = ancestors.size(); |
||||
statuses.addAll(0, ancestors); |
||||
notifyItemRangeInserted(0, statusIndex); |
||||
} |
||||
|
||||
public void addDescendants(List<Status> descendants) { |
||||
int end = statuses.size(); |
||||
statuses.addAll(descendants); |
||||
notifyItemRangeInserted(end, descendants.size()); |
||||
} |
||||
} |
||||
@ -0,0 +1,68 @@
|
||||
/* 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.content.Intent; |
||||
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; |
||||
import android.view.Menu; |
||||
import android.view.MenuItem; |
||||
|
||||
public class ViewThreadActivity extends AppCompatActivity { |
||||
|
||||
@Override |
||||
protected void onCreate(@Nullable Bundle savedInstanceState) { |
||||
super.onCreate(savedInstanceState); |
||||
setContentView(R.layout.activity_view_thread); |
||||
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); |
||||
setSupportActionBar(toolbar); |
||||
ActionBar bar = getSupportActionBar(); |
||||
if (bar != null) { |
||||
bar.setTitle(R.string.title_thread); |
||||
} |
||||
|
||||
String id = getIntent().getStringExtra("id"); |
||||
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); |
||||
Fragment fragment = ViewThreadFragment.newInstance(id); |
||||
fragmentTransaction.add(R.id.fragment_container, fragment); |
||||
fragmentTransaction.commit(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean onCreateOptionsMenu(Menu menu) { |
||||
getMenuInflater().inflate(R.menu.view_thread_toolbar, menu); |
||||
return super.onCreateOptionsMenu(menu); |
||||
} |
||||
|
||||
@Override |
||||
public boolean onOptionsItemSelected(MenuItem item) { |
||||
switch (item.getItemId()) { |
||||
case R.id.action_back: { |
||||
Intent intent = new Intent(this, MainActivity.class); |
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); |
||||
startActivity(intent); |
||||
return true; |
||||
} |
||||
} |
||||
return super.onOptionsItemSelected(item); |
||||
} |
||||
} |
||||
@ -0,0 +1,143 @@
|
||||
/* 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.content.Context; |
||||
import android.graphics.drawable.Drawable; |
||||
import android.os.Bundle; |
||||
import android.support.annotation.Nullable; |
||||
import android.support.v4.content.ContextCompat; |
||||
import android.support.v7.widget.DividerItemDecoration; |
||||
import android.support.v7.widget.LinearLayoutManager; |
||||
import android.support.v7.widget.RecyclerView; |
||||
import android.view.LayoutInflater; |
||||
import android.view.View; |
||||
import android.view.ViewGroup; |
||||
|
||||
import com.android.volley.Request; |
||||
import com.android.volley.Response; |
||||
|
||||
import org.json.JSONException; |
||||
import org.json.JSONObject; |
||||
|
||||
import java.util.List; |
||||
|
||||
public class ViewThreadFragment extends SFragment implements StatusActionListener { |
||||
private RecyclerView recyclerView; |
||||
private ThreadAdapter adapter; |
||||
|
||||
public static ViewThreadFragment newInstance(String id) { |
||||
Bundle arguments = new Bundle(); |
||||
ViewThreadFragment fragment = new ViewThreadFragment(); |
||||
arguments.putString("id", id); |
||||
fragment.setArguments(arguments); |
||||
return fragment; |
||||
} |
||||
|
||||
@Nullable |
||||
@Override |
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, |
||||
@Nullable Bundle savedInstanceState) { |
||||
View rootView = inflater.inflate(R.layout.fragment_view_thread, container, false); |
||||
|
||||
Context context = getContext(); |
||||
recyclerView = (RecyclerView) rootView.findViewById(R.id.recycler_view); |
||||
recyclerView.setHasFixedSize(true); |
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(context); |
||||
recyclerView.setLayoutManager(layoutManager); |
||||
DividerItemDecoration divider = new DividerItemDecoration( |
||||
context, layoutManager.getOrientation()); |
||||
Drawable drawable = ContextCompat.getDrawable(context, R.drawable.status_divider); |
||||
divider.setDrawable(drawable); |
||||
recyclerView.addItemDecoration(divider); |
||||
adapter = new ThreadAdapter(this); |
||||
recyclerView.setAdapter(adapter); |
||||
|
||||
String id = getArguments().getString("id"); |
||||
sendStatusRequest(id); |
||||
sendThreadRequest(id); |
||||
|
||||
return rootView; |
||||
} |
||||
|
||||
private void sendStatusRequest(String id) { |
||||
String endpoint = String.format(getString(R.string.endpoint_get_status), id); |
||||
super.sendRequest(Request.Method.GET, endpoint, null, |
||||
new Response.Listener<JSONObject>() { |
||||
@Override |
||||
public void onResponse(JSONObject response) { |
||||
Status status; |
||||
try { |
||||
status = Status.parse(response, false); |
||||
} catch (JSONException e) { |
||||
onThreadRequestFailure(); |
||||
return; |
||||
} |
||||
int position = adapter.insertStatus(status); |
||||
recyclerView.scrollToPosition(position); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
private void sendThreadRequest(String id) { |
||||
String endpoint = String.format(getString(R.string.endpoint_context), id); |
||||
super.sendRequest(Request.Method.GET, endpoint, null, |
||||
new Response.Listener<JSONObject>() { |
||||
@Override |
||||
public void onResponse(JSONObject response) { |
||||
try { |
||||
List<Status> ancestors = |
||||
Status.parse(response.getJSONArray("ancestors")); |
||||
List<Status> descendants = |
||||
Status.parse(response.getJSONArray("descendants")); |
||||
adapter.addAncestors(ancestors); |
||||
adapter.addDescendants(descendants); |
||||
} catch (JSONException e) { |
||||
onThreadRequestFailure(); |
||||
} |
||||
} |
||||
}); |
||||
} |
||||
|
||||
private void onThreadRequestFailure() { |
||||
//TODO: no
|
||||
assert(false); |
||||
} |
||||
|
||||
public void onReply(int position) { |
||||
super.reply(adapter.getItem(position)); |
||||
} |
||||
|
||||
public void onReblog(boolean reblog, int position) { |
||||
super.reblog(adapter.getItem(position), reblog, adapter, position); |
||||
} |
||||
|
||||
public void onFavourite(boolean favourite, int position) { |
||||
super.favourite(adapter.getItem(position), favourite, adapter, position); |
||||
} |
||||
|
||||
public void onMore(View view, int position) { |
||||
super.more(adapter.getItem(position), view, adapter, position); |
||||
} |
||||
|
||||
public void onViewMedia(String url, Status.MediaAttachment.Type type) { |
||||
super.viewMedia(url, type); |
||||
} |
||||
|
||||
public void onViewThread(int position) { |
||||
super.viewThread(adapter.getItem(position)); |
||||
} |
||||
} |
||||
|
Before Width: | Height: | Size: 221 B |
@ -0,0 +1,7 @@
|
||||
<vector android:height="24dp" android:viewportHeight="850.3937" |
||||
android:viewportWidth="850.3937" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
<path android:fillAlpha="1" android:fillColor="#ffffff" |
||||
android:pathData="m410.4,48.8c-9.1,0 -18.1,3.5 -25.1,10.4L84.7,359.9c-1.6,1.6 -2.9,3.2 -4.1,5 -6,6.3 -9.7,14.9 -9.7,24.4l0,70.9c0,11.3 5.2,21.3 13.4,27.8 1.6,3.2 3.8,6.2 6.5,8.9L391.5,797.5c13.9,13.9 36.2,13.9 50.1,0l50.1,-50.1c13.9,-13.9 13.9,-36.2 0,-50.1l-201.7,-201.7 454.1,0c19.6,0 35.4,-15.8 35.4,-35.4l0,-70.9c0,-19.6 -15.8,-35.4 -35.4,-35.4l-452.9,0 194.4,-194.4c13.9,-13.9 13.9,-36.2 0,-50.1l-50.1,-50.1c-6.9,-6.9 -16,-10.4 -25.1,-10.4z" |
||||
android:strokeAlpha="1" android:strokeColor="#00000000" |
||||
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="10.62992096"/> |
||||
</vector> |
||||
@ -0,0 +1,7 @@
|
||||
<vector android:height="16dp" android:viewportHeight="566.92914" |
||||
android:viewportWidth="566.92914" android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
<path android:fillAlpha="1" android:fillColor="#000000" |
||||
android:pathData="M141.7,0C63.2,0 0,63.2 0,141.7L0,425.2C0,503.7 63.2,566.9 141.7,566.9L425.2,566.9C503.7,566.9 566.9,503.7 566.9,425.2L566.9,141.7C566.9,63.2 503.7,0 425.2,0L141.7,0zM283.6,24.8C287.6,24.9 291.2,27.1 293,30.7L363.4,173.4L520.9,196.3C529.6,197.6 533.1,208.3 526.8,214.4L412.8,325.5L439.7,482.3C441.2,491 432.1,497.6 424.3,493.5L283.5,419.5L142.6,493.5C134.8,497.6 125.7,491 127.2,482.3L154.1,325.5L40.2,214.4C33.8,208.3 37.3,197.6 46,196.3L203.5,173.4L273.9,30.7C275.7,27.1 279.5,24.8 283.6,24.8z" |
||||
android:strokeAlpha="1" android:strokeColor="#9d9d9d" |
||||
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="0"/> |
||||
</vector> |
||||
@ -0,0 +1,7 @@
|
||||
<vector android:height="16dp" android:viewportHeight="566.92914" |
||||
android:viewportWidth="566.92914" android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
<path android:fillAlpha="1" android:fillColor="#000000" |
||||
android:pathData="M141.7,0C63.2,0 0,63.2 0,141.7L0,425.2C0,503.7 63.2,566.9 141.7,566.9L425.2,566.9C503.7,566.9 566.9,503.7 566.9,425.2L566.9,141.7C566.9,63.2 503.7,0 425.2,0L141.7,0zM283.5,70.9A88.6,88.6 0,0 1,372 159.4A88.6,88.6 0,0 1,283.5 248A88.6,88.6 0,0 1,194.9 159.4A88.6,88.6 0,0 1,283.5 70.9zM194.9,311.3C194.9,311.3 229.1,336.6 283.5,336.6C338.4,336.6 370.5,311.3 370.5,311.3C496.1,407.5 460.6,478.3 460.6,478.3L106.3,478.3C106.3,478.3 70.9,407.5 194.9,311.3z" |
||||
android:strokeAlpha="1" android:strokeColor="#9d9d9d" |
||||
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="0"/> |
||||
</vector> |
||||
@ -0,0 +1,7 @@
|
||||
<vector android:height="16dp" android:viewportHeight="566.92914" |
||||
android:viewportWidth="566.92914" android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
<path android:fillAlpha="1" android:fillColor="#000000" |
||||
android:pathData="M141.7,0C63.2,0 0,63.2 0,141.7L0,425.2C0,503.7 63.2,566.9 141.7,566.9L425.2,566.9C503.7,566.9 566.9,503.7 566.9,425.2L566.9,141.7C566.9,63.2 503.7,0 425.2,0L141.7,0zM177.2,124L354.3,124C432.9,124 496.1,182.6 496.1,265.7L496.1,336.6L549.2,336.6L460.6,425.2L372,336.6L425.2,336.6L425.2,265.7C425.2,226.4 393.6,194.9 354.3,194.9L248,194.9L177.2,124zM106.3,141.7L194.9,230.3L141.7,230.3L141.7,301.2C141.7,340.5 173.3,372 212.6,372L318.9,372L389.8,442.9L212.6,442.9C134,442.9 70.9,384.3 70.9,301.2L70.9,230.3L17.7,230.3L106.3,141.7z" |
||||
android:strokeAlpha="1" android:strokeColor="#9d9d9d" |
||||
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="0"/> |
||||
</vector> |
||||
@ -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.ViewThreadActivity"> |
||||
|
||||
<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> |
||||
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<android.support.v7.widget.RecyclerView |
||||
xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:id="@+id/recycler_view" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent" |
||||
android:scrollbars="vertical" /> |
||||
@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||
xmlns:app="http://schemas.android.com/apk/res-auto" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="wrap_content"> |
||||
|
||||
<RelativeLayout |
||||
android:layout_width="@dimen/status_avatar_column_width" |
||||
android:layout_height="wrap_content" |
||||
android:id="@+id/notification_side_column"> |
||||
|
||||
<ImageView |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
app:srcCompat="@drawable/ic_followed" |
||||
android:paddingTop="@dimen/notification_icon_vertical_padding" |
||||
android:paddingBottom="@dimen/notification_icon_vertical_padding" |
||||
android:paddingRight="@dimen/status_avatar_padding" |
||||
android:layout_alignParentRight="true" /> |
||||
|
||||
</RelativeLayout> |
||||
|
||||
<TextView |
||||
android:layout_toRightOf="@id/notification_side_column" |
||||
android:id="@+id/notification_text" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="wrap_content" |
||||
android:paddingBottom="@dimen/notification_icon_vertical_padding" |
||||
android:layout_alignParentBottom="true" /> |
||||
|
||||
</RelativeLayout> |
||||
@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:orientation="vertical" android:layout_width="match_parent" |
||||
android:layout_height="wrap_content"> |
||||
|
||||
<TextView |
||||
android:id="@+id/notification_text" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="wrap_content" /> |
||||
|
||||
</LinearLayout> |
||||
@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:orientation="horizontal" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="wrap_content"> |
||||
|
||||
<RelativeLayout |
||||
android:layout_width="@dimen/status_avatar_column_width" |
||||
android:layout_height="wrap_content" |
||||
android:id="@+id/notification_side_column"> |
||||
|
||||
<ImageView |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:id="@+id/notification_icon" |
||||
android:paddingTop="@dimen/notification_icon_vertical_padding" |
||||
android:paddingBottom="@dimen/notification_icon_vertical_padding" |
||||
android:paddingRight="@dimen/status_avatar_padding" |
||||
android:layout_alignParentRight="true" /> |
||||
|
||||
</RelativeLayout> |
||||
|
||||
<TextView |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:id="@+id/notification_text" |
||||
android:layout_toRightOf="@id/notification_side_column" |
||||
android:paddingBottom="@dimen/notification_icon_vertical_padding" /> |
||||
|
||||
<TextView |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:id="@+id/notification_content" |
||||
android:layout_toRightOf="@id/notification_side_column" |
||||
android:layout_below="@id/notification_text" |
||||
android:textColor="@color/notification_content_faded" |
||||
android:paddingBottom="8dp" /> |
||||
|
||||
</RelativeLayout> |
||||
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<menu |
||||
xmlns:android="http://schemas.android.com/apk/res/android" |
||||
xmlns:app="http://schemas.android.com/apk/res-auto"> |
||||
|
||||
<item android:id="@+id/action_back" |
||||
android:title="@string/action_back" |
||||
android:icon="@drawable/ic_back" |
||||
app:showAsAction="always" /> |
||||
|
||||
</menu> |
||||
Loading…
Reference in new issue