Support Mastodon notification bell and status notification type
This commit is contained in:
parent
7808f122e0
commit
68d6a4ab25
@ -327,7 +327,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||
* Subscribe to data loaded at the view model
|
||||
*/
|
||||
private fun subscribeObservables() {
|
||||
viewModel.accountData.observe(this, Observer<Resource<Account>> {
|
||||
viewModel.accountData.observe(this) {
|
||||
when (it) {
|
||||
is Success -> onAccountChanged(it.data)
|
||||
is Error -> {
|
||||
@ -336,8 +336,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||
.show()
|
||||
}
|
||||
}
|
||||
})
|
||||
viewModel.relationshipData.observe(this, Observer<Resource<Relationship>> {
|
||||
}
|
||||
viewModel.relationshipData.observe(this) {
|
||||
val relation = it?.data
|
||||
if (relation != null) {
|
||||
onRelationshipChanged(relation)
|
||||
@ -349,11 +349,11 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||
.show()
|
||||
}
|
||||
|
||||
})
|
||||
viewModel.accountFieldData.observe(this, Observer<List<Either<IdentityProof, Field>>> {
|
||||
}
|
||||
viewModel.accountFieldData.observe(this) {
|
||||
accountFieldAdapter.fields = it
|
||||
accountFieldAdapter.notifyDataSetChanged()
|
||||
})
|
||||
}
|
||||
viewModel.noteSaved.observe(this) {
|
||||
saveNoteInfo.visible(it, View.INVISIBLE)
|
||||
}
|
||||
@ -367,9 +367,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||
viewModel.refresh()
|
||||
adapter.refreshContent()
|
||||
}
|
||||
viewModel.isRefreshing.observe(this, Observer { isRefreshing ->
|
||||
viewModel.isRefreshing.observe(this) { isRefreshing ->
|
||||
swipeToRefreshLayout.isRefreshing = isRefreshing == true
|
||||
})
|
||||
}
|
||||
swipeToRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
|
||||
}
|
||||
|
||||
@ -436,7 +436,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||
|
||||
|
||||
accountAvatarImageView.setOnClickListener { avatarView ->
|
||||
val intent = ViewMediaActivity.newAvatarIntent(avatarView.context, account.avatar)
|
||||
val intent = ViewMediaActivity.newSingleImageIntent(avatarView.context, account.avatar)
|
||||
|
||||
avatarView.transitionName = account.avatar
|
||||
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this, avatarView, account.avatar)
|
||||
@ -635,11 +635,16 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||
accountFollowsYouTextView.visible(relation.followedBy)
|
||||
|
||||
// because subscribing is Pleroma extension, enable it __only__ when we have non-null subscribing field
|
||||
if(!viewModel.isSelf && followState == FollowState.FOLLOWING && relation.subscribing != null) {
|
||||
// it's also now supported in Mastodon 3.3.0rc but called notifying and use different API call
|
||||
if(!viewModel.isSelf && followState == FollowState.FOLLOWING
|
||||
&& (relation.subscribing != null || relation.notifying != null)) {
|
||||
accountSubscribeButton.show()
|
||||
accountSubscribeButton.setOnClickListener {
|
||||
viewModel.changeSubscribingState()
|
||||
}
|
||||
if(relation.notifying != null)
|
||||
subscribing = relation.notifying
|
||||
else if(relation.subscribing != null)
|
||||
subscribing = relation.subscribing
|
||||
}
|
||||
|
||||
|
@ -88,7 +88,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
||||
return intent
|
||||
}
|
||||
|
||||
fun newAvatarIntent(context: Context?, url: String): Intent {
|
||||
fun newSingleImageIntent(context: Context?, url: String): Intent {
|
||||
val intent = Intent(context, ViewMediaActivity::class.java)
|
||||
intent.putExtra(EXTRA_AVATAR_URL, url)
|
||||
return intent
|
||||
|
@ -39,6 +39,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.entity.Account;
|
||||
import com.keylesspalace.tusky.entity.Emoji;
|
||||
@ -216,9 +217,13 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||
holder.setUsername(statusViewData.getNickname());
|
||||
holder.setCreatedAt(statusViewData.getCreatedAt());
|
||||
|
||||
holder.setAvatars(concreteNotificaton.getStatusViewData().getAvatar(),
|
||||
if(concreteNotificaton.getType() == Notification.Type.STATUS) {
|
||||
holder.setAvatar(statusViewData.getAvatar(), statusViewData.isBot());
|
||||
} else {
|
||||
holder.setAvatars(statusViewData.getAvatar(),
|
||||
concreteNotificaton.getAccount().getAvatar());
|
||||
}
|
||||
}
|
||||
|
||||
holder.setMessage(concreteNotificaton, statusListener);
|
||||
holder.setupButtons(notificationActionListener,
|
||||
@ -291,10 +296,11 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||
switch (concrete.getType()) {
|
||||
case MENTION:
|
||||
case POLL: {
|
||||
if(concrete.getStatusViewData() != null && concrete.getStatusViewData().isMuted())
|
||||
if (concrete.getStatusViewData() != null && concrete.getStatusViewData().isMuted())
|
||||
return VIEW_TYPE_MUTED_STATUS;
|
||||
return VIEW_TYPE_STATUS;
|
||||
}
|
||||
case STATUS:
|
||||
case FAVOURITE:
|
||||
case REBLOG:
|
||||
case EMOJI_REACTION: {
|
||||
@ -427,6 +433,10 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||
private SimpleDateFormat shortSdf;
|
||||
private SimpleDateFormat longSdf;
|
||||
|
||||
private int avatarRadius48dp;
|
||||
private int avatarRadius36dp;
|
||||
private int avatarRadius24dp;
|
||||
|
||||
StatusNotificationViewHolder(View itemView, StatusDisplayOptions statusDisplayOptions) {
|
||||
super(itemView);
|
||||
message = itemView.findViewById(R.id.notification_top_text);
|
||||
@ -452,6 +462,10 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||
statusContent.setOnClickListener(this);
|
||||
shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
|
||||
longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault());
|
||||
|
||||
this.avatarRadius48dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_48dp);
|
||||
this.avatarRadius36dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_36dp);
|
||||
this.avatarRadius24dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_24dp);
|
||||
}
|
||||
|
||||
private void showNotificationContent(boolean show) {
|
||||
@ -460,7 +474,6 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||
contentWarningButton.setVisibility(show ? View.VISIBLE : View.GONE);
|
||||
statusContent.setVisibility(show ? View.VISIBLE : View.GONE);
|
||||
statusAvatar.setVisibility(show ? View.VISIBLE : View.GONE);
|
||||
notificationAvatar.setVisibility(show ? View.VISIBLE : View.GONE);
|
||||
replyInfo.setVisibility(show ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
@ -545,6 +558,17 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||
wholeMessage = String.format(format, displayName);
|
||||
break;
|
||||
}
|
||||
case STATUS: {
|
||||
icon = ContextCompat.getDrawable(context, R.drawable.ic_home_24dp);
|
||||
if (icon != null) {
|
||||
icon.setColorFilter(ContextCompat.getColor(context,
|
||||
R.color.tusky_blue), PorterDuff.Mode.SRC_ATOP);
|
||||
}
|
||||
|
||||
String format = context.getString(R.string.notification_subscription_format);
|
||||
wholeMessage = String.format(format, displayName);
|
||||
break;
|
||||
}
|
||||
case EMOJI_REACTION: {
|
||||
icon = ContextCompat.getDrawable(context, R.drawable.ic_emoji_24dp);
|
||||
if(icon != null) {
|
||||
@ -595,19 +619,34 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||
this.notificationId = notificationId;
|
||||
}
|
||||
|
||||
void setAvatars(@Nullable String statusAvatarUrl, @Nullable String notificationAvatarUrl) {
|
||||
|
||||
int statusAvatarRadius = statusAvatar.getContext().getResources()
|
||||
.getDimensionPixelSize(R.dimen.avatar_radius_36dp);
|
||||
void setAvatar(@Nullable String statusAvatarUrl, boolean isBot) {
|
||||
statusAvatar.setPaddingRelative(0, 0, 0, 0);
|
||||
|
||||
ImageLoadingHelper.loadAvatar(statusAvatarUrl,
|
||||
statusAvatar, statusAvatarRadius, statusDisplayOptions.animateAvatars());
|
||||
statusAvatar, avatarRadius48dp, statusDisplayOptions.animateAvatars());
|
||||
|
||||
int notificationAvatarRadius = statusAvatar.getContext().getResources()
|
||||
.getDimensionPixelSize(R.dimen.avatar_radius_24dp);
|
||||
if (statusDisplayOptions.showBotOverlay() && isBot) {
|
||||
notificationAvatar.setVisibility(View.VISIBLE);
|
||||
notificationAvatar.setBackgroundColor(0x50ffffff);
|
||||
Glide.with(notificationAvatar)
|
||||
.load(R.drawable.ic_bot_24dp)
|
||||
.into(notificationAvatar);
|
||||
|
||||
} else {
|
||||
notificationAvatar.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
void setAvatars(@Nullable String statusAvatarUrl, @Nullable String notificationAvatarUrl) {
|
||||
int padding = Utils.dpToPx(statusAvatar.getContext(), 12);
|
||||
statusAvatar.setPaddingRelative(0, 0, padding, padding);
|
||||
|
||||
ImageLoadingHelper.loadAvatar(statusAvatarUrl,
|
||||
statusAvatar, avatarRadius36dp, statusDisplayOptions.animateAvatars());
|
||||
|
||||
notificationAvatar.setVisibility(View.VISIBLE);
|
||||
ImageLoadingHelper.loadAvatar(notificationAvatarUrl, notificationAvatar,
|
||||
notificationAvatarRadius, statusDisplayOptions.animateAvatars());
|
||||
avatarRadius24dp, statusDisplayOptions.animateAvatars());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -152,6 +152,7 @@ public class NotificationHelper {
|
||||
*/
|
||||
|
||||
public static void make(final Context context, Notification body, AccountEntity account, boolean isFirstOfBatch) {
|
||||
body = Notification.rewriteToStatusTypeIfNeeded(body, account.getAccountId());
|
||||
|
||||
if (!filterNotification(account, body, context)) {
|
||||
return;
|
||||
@ -572,9 +573,9 @@ public class NotificationHelper {
|
||||
|
||||
switch (notification.getType()) {
|
||||
case MENTION:
|
||||
if(isMentionedInNotification(notification, account.getAccountId()))
|
||||
return account.getNotificationsMentioned();
|
||||
else return account.getNotificationsSubscriptions();
|
||||
case STATUS:
|
||||
return account.getNotificationsSubscriptions();
|
||||
case FOLLOW:
|
||||
return account.getNotificationsFollowed();
|
||||
case FOLLOW_REQUEST:
|
||||
@ -600,9 +601,9 @@ public class NotificationHelper {
|
||||
private static String getChannelId(AccountEntity account, Notification notification) {
|
||||
switch (notification.getType()) {
|
||||
case MENTION:
|
||||
if(isMentionedInNotification(notification, account.getAccountId()))
|
||||
return CHANNEL_MENTION + account.getIdentifier();
|
||||
else return CHANNEL_SUBSCRIPTIONS + account.getIdentifier();
|
||||
case STATUS:
|
||||
return CHANNEL_SUBSCRIPTIONS + account.getIdentifier();
|
||||
case FOLLOW:
|
||||
return CHANNEL_FOLLOW + account.getIdentifier();
|
||||
case FOLLOW_REQUEST:
|
||||
@ -672,31 +673,16 @@ public class NotificationHelper {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isMentionedInNotification(Notification not, String id) {
|
||||
if(not.getStatus() != null) {
|
||||
for(int i = 0; i < not.getStatus().getMentions().length; i++) {
|
||||
if(not.getStatus().getMentions()[i].getId().equals(id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true; // actually should never happen, true just in case someone breaks API again
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String titleForType(Context context, Notification notification, AccountEntity account) {
|
||||
String accountName = StringUtils.unicodeWrap(notification.getAccount().getName());
|
||||
switch (notification.getType()) {
|
||||
case MENTION:
|
||||
if(isMentionedInNotification(notification, account.getAccountId())) {
|
||||
return String.format(context.getString(R.string.notification_mention_format),
|
||||
accountName);
|
||||
} else {
|
||||
return String.format(context.getString(R.string.notification_subscription_format), accountName);
|
||||
}
|
||||
case STATUS:
|
||||
return String.format(context.getString(R.string.notification_subscription_format),
|
||||
accountName);
|
||||
case FOLLOW:
|
||||
return String.format(context.getString(R.string.notification_follow_format),
|
||||
accountName);
|
||||
@ -739,6 +725,7 @@ public class NotificationHelper {
|
||||
case FAVOURITE:
|
||||
case REBLOG:
|
||||
case EMOJI_REACTION:
|
||||
case STATUS:
|
||||
if (!TextUtils.isEmpty(notification.getStatus().getSpoilerText())) {
|
||||
return notification.getStatus().getSpoilerText();
|
||||
} else {
|
||||
|
@ -46,7 +46,8 @@ data class Notification(
|
||||
EMOJI_REACTION("pleroma:emoji_reaction"),
|
||||
FOLLOW_REQUEST("follow_request"),
|
||||
CHAT_MESSAGE("pleroma:chat_mention"),
|
||||
MOVE("move");
|
||||
MOVE("move"),
|
||||
STATUS("status"); /* Mastodon 3.3.0rc1 */
|
||||
|
||||
companion object {
|
||||
|
||||
@ -58,7 +59,7 @@ data class Notification(
|
||||
}
|
||||
return UNKNOWN
|
||||
}
|
||||
val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, POLL, EMOJI_REACTION, FOLLOW_REQUEST, CHAT_MESSAGE, MOVE)
|
||||
val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, POLL, EMOJI_REACTION, FOLLOW_REQUEST, CHAT_MESSAGE, MOVE, STATUS)
|
||||
|
||||
val asStringList = asList.map { it.presentation }
|
||||
}
|
||||
@ -88,4 +89,19 @@ data class Notification(
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
// for Pleroma compatibility that uses Mention type
|
||||
@JvmStatic
|
||||
fun rewriteToStatusTypeIfNeeded(body: Notification, accountId: String) : Notification {
|
||||
if (body.type == Type.MENTION
|
||||
&& body.status != null) {
|
||||
return if (body.status.mentions.any {
|
||||
it.id == accountId
|
||||
}) body else body.copy(type = Type.STATUS)
|
||||
}
|
||||
return body
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,5 +28,6 @@ data class Relationship (
|
||||
@SerializedName("showing_reblogs") val showingReblogs: Boolean,
|
||||
val subscribing: Boolean? = null, // Pleroma extension
|
||||
@SerializedName("domain_blocking") val blockingDomain: Boolean,
|
||||
val note: String? // nullable for backward compatibility / feature detection
|
||||
val note: String?, // nullable for backward compatibility / feature detection
|
||||
val notifying: Boolean? // since 3.3.0rc
|
||||
)
|
||||
|
@ -177,7 +177,10 @@ public class NotificationsFragment extends SFragment implements
|
||||
@Override
|
||||
public NotificationViewData apply(Either<Placeholder, Notification> input) {
|
||||
if (input.isRight()) {
|
||||
Notification notification = input.asRight();
|
||||
Notification notification = Notification.rewriteToStatusTypeIfNeeded(
|
||||
input.asRight(), accountManager.getActiveAccount().getAccountId()
|
||||
);
|
||||
|
||||
return ViewDataUtils.notificationToViewData(
|
||||
notification,
|
||||
alwaysShowSensitiveMedia,
|
||||
@ -874,6 +877,8 @@ public class NotificationsFragment extends SFragment implements
|
||||
return getString(R.string.notification_emoji_name);
|
||||
case MOVE:
|
||||
return getString(R.string.notification_move_name);
|
||||
case STATUS:
|
||||
return getString(R.string.notification_subscription_name);
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
|
@ -309,7 +309,8 @@ interface MastodonApi {
|
||||
@POST("api/v1/accounts/{id}/follow")
|
||||
fun followAccount(
|
||||
@Path("id") accountId: String,
|
||||
@Field("reblogs") showReblogs: Boolean
|
||||
@Field("reblogs") showReblogs: Boolean? = null,
|
||||
@Field("notify") notify: Boolean? = null
|
||||
): Single<Relationship>
|
||||
|
||||
@POST("api/v1/accounts/{id}/unfollow")
|
||||
|
@ -33,7 +33,7 @@ class AccountViewModel @Inject constructor(
|
||||
|
||||
val accountFieldData = combineOptionalLiveData(accountData, identityProofData) { accountRes, identityProofs ->
|
||||
identityProofs.orEmpty().map { Either.Left<IdentityProof, Field>(it) }
|
||||
.plus(accountRes?.data?.fields.orEmpty().map { Either.Right<IdentityProof, Field>(it) })
|
||||
.plus(accountRes?.data?.fields.orEmpty().map { Either.Right(it) })
|
||||
}
|
||||
|
||||
val isRefreshing = MutableLiveData<Boolean>().apply { value = false }
|
||||
@ -128,7 +128,9 @@ class AccountViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun changeSubscribingState() {
|
||||
if (relationshipData.value?.data?.subscribing == true) {
|
||||
val relationship = relationshipData.value?.data
|
||||
if(relationship?.notifying == true /* Mastodon 3.3.0rc1 */
|
||||
|| relationship?.subscribing == true /* Pleroma */ ) {
|
||||
changeRelationship(RelationShipAction.UNSUBSCRIBE)
|
||||
} else {
|
||||
changeRelationship(RelationShipAction.SUBSCRIBE)
|
||||
@ -188,6 +190,7 @@ class AccountViewModel @Inject constructor(
|
||||
private fun changeRelationship(relationshipAction: RelationShipAction, parameter: Boolean? = null) {
|
||||
val relation = relationshipData.value?.data
|
||||
val account = accountData.value?.data
|
||||
val isMastodon = relationshipData.value?.data?.notifying != null
|
||||
|
||||
if (relation != null && account != null) {
|
||||
// optimistically post new state for faster response
|
||||
@ -205,21 +208,37 @@ class AccountViewModel @Inject constructor(
|
||||
RelationShipAction.UNBLOCK -> relation.copy(blocking = false)
|
||||
RelationShipAction.MUTE -> relation.copy(muting = true)
|
||||
RelationShipAction.UNMUTE -> relation.copy(muting = false)
|
||||
RelationShipAction.SUBSCRIBE -> relation.copy(subscribing = true)
|
||||
RelationShipAction.UNSUBSCRIBE -> relation.copy(subscribing = false)
|
||||
RelationShipAction.SUBSCRIBE -> {
|
||||
if(isMastodon)
|
||||
relation.copy(notifying = true)
|
||||
else relation.copy(subscribing = true)
|
||||
}
|
||||
RelationShipAction.UNSUBSCRIBE -> {
|
||||
if(isMastodon)
|
||||
relation.copy(notifying = false)
|
||||
else relation.copy(subscribing = false)
|
||||
}
|
||||
}
|
||||
relationshipData.postValue(Loading(newRelation))
|
||||
}
|
||||
|
||||
when (relationshipAction) {
|
||||
RelationShipAction.FOLLOW -> mastodonApi.followAccount(accountId, parameter ?: true)
|
||||
RelationShipAction.FOLLOW -> mastodonApi.followAccount(accountId, showReblogs = parameter ?: true)
|
||||
RelationShipAction.UNFOLLOW -> mastodonApi.unfollowAccount(accountId)
|
||||
RelationShipAction.BLOCK -> mastodonApi.blockAccount(accountId)
|
||||
RelationShipAction.UNBLOCK -> mastodonApi.unblockAccount(accountId)
|
||||
RelationShipAction.MUTE -> mastodonApi.muteAccount(accountId, parameter ?: true)
|
||||
RelationShipAction.UNMUTE -> mastodonApi.unmuteAccount(accountId)
|
||||
RelationShipAction.SUBSCRIBE -> mastodonApi.subscribeAccount(accountId)
|
||||
RelationShipAction.UNSUBSCRIBE -> mastodonApi.unsubscribeAccount(accountId)
|
||||
RelationShipAction.SUBSCRIBE -> {
|
||||
if(isMastodon)
|
||||
mastodonApi.followAccount(accountId, notify = true)
|
||||
else mastodonApi.subscribeAccount(accountId)
|
||||
}
|
||||
RelationShipAction.UNSUBSCRIBE -> {
|
||||
if(isMastodon)
|
||||
mastodonApi.followAccount(accountId, notify = false)
|
||||
else mastodonApi.unsubscribeAccount(accountId)
|
||||
}
|
||||
}.subscribe(
|
||||
{ relationship ->
|
||||
relationshipData.postValue(Success(relationship))
|
||||
|
@ -164,8 +164,6 @@
|
||||
android:layout_marginRight="14dp"
|
||||
android:layout_marginBottom="14dp"
|
||||
android:contentDescription="@string/action_view_profile"
|
||||
android:paddingRight="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:scaleType="centerCrop"
|
||||
tools:ignore="RtlHardcoded,RtlSymmetry"
|
||||
tools:src="@drawable/avatar_default" />
|
||||
|
Loading…
Reference in New Issue
Block a user