diff --git a/src/main/java/eu/siacs/conversations/AppSettings.java b/src/main/java/eu/siacs/conversations/AppSettings.java index c1d021af86345d8d1aa480fc5e593d97440de0f5..4287d694f69fc51fe3ba976fcc32e39ad0aecdf2 100644 --- a/src/main/java/eu/siacs/conversations/AppSettings.java +++ b/src/main/java/eu/siacs/conversations/AppSettings.java @@ -44,6 +44,7 @@ public class AppSettings { public static final String LARGE_FONT = "large_font"; public static final String SHOW_AVATARS = "show_avatars"; public static final String CALL_INTEGRATION = "call_integration"; + public static final String ALIGN_START = "align_start"; private static final String ACCEPT_INVITES_FROM_STRANGERS = "accept_invites_from_strangers"; private static final String INSTALLATION_ID = "im.conversations.android.install_id"; @@ -115,6 +116,10 @@ public class AppSettings { return getBooleanPreference(CALL_INTEGRATION, R.bool.call_integration); } + public boolean isAlignStart() { + return getBooleanPreference(ALIGN_START, R.bool.align_start); + } + public boolean isUseTor() { return getBooleanPreference(USE_TOR, R.bool.use_tor); } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index e6437367a0c9ed2fa33420d56f51ccaf19eb5ecd..7eb8ea05ca7f4074585c260639197925bf0a5707 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -48,9 +48,9 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; import eu.siacs.conversations.databinding.ItemMessageDateBubbleBinding; -import eu.siacs.conversations.databinding.ItemMessageReceivedBinding; +import eu.siacs.conversations.databinding.ItemMessageEndBinding; import eu.siacs.conversations.databinding.ItemMessageRtpSessionBinding; -import eu.siacs.conversations.databinding.ItemMessageSentBinding; +import eu.siacs.conversations.databinding.ItemMessageStartBinding; import eu.siacs.conversations.databinding.ItemMessageStatusBinding; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; @@ -97,8 +97,8 @@ import java.util.regex.Pattern; public class MessageAdapter extends ArrayAdapter { public static final String DATE_SEPARATOR_BODY = "DATE_SEPARATOR"; - private static final int SENT = 0; - private static final int RECEIVED = 1; + private static final int END = 0; + private static final int START = 1; private static final int STATUS = 2; private static final int DATE_SEPARATOR = 3; private static final int RTP_SESSION = 4; @@ -108,7 +108,7 @@ public class MessageAdapter extends ArrayAdapter { private final DisplayMetrics metrics; private OnContactPictureClicked mOnContactPictureClickedListener; private OnContactPictureLongClicked mOnContactPictureLongClickedListener; - private BubbleDesign bubbleDesign = new BubbleDesign(false, false, true); + private BubbleDesign bubbleDesign = new BubbleDesign(false, false, false, true); private final boolean mForceNames; public MessageAdapter( @@ -160,7 +160,7 @@ public class MessageAdapter extends ArrayAdapter { return 5; } - private static int getItemViewType(final Message message) { + private static int getItemViewType(final Message message, final boolean alignStart) { if (message.getType() == Message.TYPE_STATUS) { if (DATE_SEPARATOR_BODY.equals(message.getBody())) { return DATE_SEPARATOR; @@ -169,29 +169,29 @@ public class MessageAdapter extends ArrayAdapter { } } else if (message.getType() == Message.TYPE_RTP_SESSION) { return RTP_SESSION; - } else if (message.getStatus() <= Message.STATUS_RECEIVED) { - return RECEIVED; + } else if (message.getStatus() <= Message.STATUS_RECEIVED || alignStart) { + return START; } else { - return SENT; + return END; } } @Override public int getItemViewType(final int position) { - return getItemViewType(getItem(position)); + return getItemViewType(getItem(position), bubbleDesign.alignStart); } private void displayStatus( final BubbleMessageItemViewHolder viewHolder, final Message message, - final int type, final BubbleColor bubbleColor) { - final int mergedStatus = message.getStatus(); + final int status = message.getStatus(); final boolean error; final Transferable transferable = message.getTransferable(); - final boolean multiReceived = + final boolean sent = status != Message.STATUS_RECEIVED; + final boolean showUserNickname = message.getConversation().getMode() == Conversation.MODE_MULTI - && mergedStatus <= Message.STATUS_RECEIVED; + && viewHolder instanceof StartBubbleMessageItemViewHolder; final String fileSize; if (message.isFileOrImage() || transferable != null @@ -211,24 +211,27 @@ public class MessageAdapter extends ArrayAdapter { fileSize = null; error = message.getStatus() == Message.STATUS_SEND_FAILED; } - if (type == SENT && viewHolder instanceof EndBubbleMessageItemViewHolder endViewHolder) { + + if (sent) { final @DrawableRes Integer receivedIndicator = - getMessageStatusAsDrawable(message, mergedStatus); + getMessageStatusAsDrawable(message, status); if (receivedIndicator == null) { - endViewHolder.indicatorReceived().setVisibility(View.INVISIBLE); + viewHolder.indicatorReceived().setVisibility(View.INVISIBLE); } else { - endViewHolder.indicatorReceived().setImageResource(receivedIndicator); - if (mergedStatus == Message.STATUS_SEND_FAILED) { - setImageTintError(endViewHolder.indicatorReceived()); + viewHolder.indicatorReceived().setImageResource(receivedIndicator); + if (status == Message.STATUS_SEND_FAILED) { + setImageTintError(viewHolder.indicatorReceived()); } else { - setImageTint(endViewHolder.indicatorReceived(), bubbleColor); + setImageTint(viewHolder.indicatorReceived(), bubbleColor); } - endViewHolder.indicatorReceived().setVisibility(View.VISIBLE); + viewHolder.indicatorReceived().setVisibility(View.VISIBLE); } + } else { + viewHolder.indicatorReceived().setVisibility(View.GONE); } - final var additionalStatusInfo = getAdditionalStatusInfo(message, mergedStatus); + final var additionalStatusInfo = getAdditionalStatusInfo(message, status); - if (error && type == SENT) { + if (error && sent) { viewHolder .time() .setTextColor( @@ -239,78 +242,69 @@ public class MessageAdapter extends ArrayAdapter { setTextColor(viewHolder.time(), bubbleColor); } if (message.getEncryption() == Message.ENCRYPTION_NONE) { - viewHolder.indicator().setVisibility(View.GONE); + viewHolder.indicatorSecurity().setVisibility(View.GONE); } else { boolean verified = false; if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { - final FingerprintStatus status = + final FingerprintStatus fingerprintStatus = message.getConversation() .getAccount() .getAxolotlService() .getFingerprintTrust(message.getFingerprint()); - if (status != null && status.isVerified()) { + if (fingerprintStatus != null && fingerprintStatus.isVerified()) { verified = true; } } if (verified) { - viewHolder.indicator().setImageResource(R.drawable.ic_verified_user_24dp); + viewHolder.indicatorSecurity().setImageResource(R.drawable.ic_verified_user_24dp); } else { - viewHolder.indicator().setImageResource(R.drawable.ic_lock_24dp); + viewHolder.indicatorSecurity().setImageResource(R.drawable.ic_lock_24dp); } - if (error && type == SENT) { - setImageTintError(viewHolder.indicator()); + if (error && sent) { + setImageTintError(viewHolder.indicatorSecurity()); } else { - setImageTint(viewHolder.indicator(), bubbleColor); + setImageTint(viewHolder.indicatorSecurity(), bubbleColor); } - viewHolder.indicator().setVisibility(View.VISIBLE); + viewHolder.indicatorSecurity().setVisibility(View.VISIBLE); } if (message.edited()) { - viewHolder.editIndicator().setVisibility(View.VISIBLE); - if (error && type == SENT) { - setImageTintError(viewHolder.editIndicator()); + viewHolder.indicatorEdit().setVisibility(View.VISIBLE); + if (error && sent) { + setImageTintError(viewHolder.indicatorEdit()); } else { - setImageTint(viewHolder.editIndicator(), bubbleColor); + setImageTint(viewHolder.indicatorEdit(), bubbleColor); } } else { - viewHolder.editIndicator().setVisibility(View.GONE); + viewHolder.indicatorEdit().setVisibility(View.GONE); } final String formattedTime = UIHelper.readableTimeDifferenceFull(getContext(), message.getTimeSent()); final String bodyLanguage = message.getBodyLanguage(); final ImmutableList.Builder timeInfoBuilder = new ImmutableList.Builder<>(); - if (message.getStatus() <= Message.STATUS_RECEIVED) { - timeInfoBuilder.add(formattedTime); - if (fileSize != null) { - timeInfoBuilder.add(fileSize); - } - if (mForceNames || multiReceived) { - final String displayName = UIHelper.getMessageDisplayName(message); - if (displayName != null) { - timeInfoBuilder.add(displayName); - } - } - if (bodyLanguage != null) { - timeInfoBuilder.add(bodyLanguage.toUpperCase(Locale.US)); + + if (mForceNames || showUserNickname) { + final String displayName = UIHelper.getMessageDisplayName(message); + if (displayName != null) { + timeInfoBuilder.add(displayName); } + } + if (fileSize != null) { + timeInfoBuilder.add(fileSize); + } + if (bodyLanguage != null) { + timeInfoBuilder.add(bodyLanguage.toUpperCase(Locale.US)); + } + // for space reasons we display only 'additional status info' (send progress or concrete + // failure reason) or the time + if (additionalStatusInfo != null) { + timeInfoBuilder.add(additionalStatusInfo); } else { - if (bodyLanguage != null) { - timeInfoBuilder.add(bodyLanguage.toUpperCase(Locale.US)); - } - if (fileSize != null) { - timeInfoBuilder.add(fileSize); - } - // for space reasons we display only 'additional status info' (send progress or concrete - // failure reason) or the time - if (additionalStatusInfo != null) { - timeInfoBuilder.add(additionalStatusInfo); - } else { - timeInfoBuilder.add(formattedTime); - } + timeInfoBuilder.add(formattedTime); } final var timeInfo = timeInfoBuilder.build(); - viewHolder.time().setText(Joiner.on(" \u00B7 ").join(timeInfo)); + viewHolder.time().setText(Joiner.on(" ยท ").join(timeInfo)); } public static @DrawableRes Integer getMessageStatusAsDrawable( @@ -779,18 +773,18 @@ public class MessageAdapter extends ArrayAdapter { R.layout.item_message_status, parent, false)); - case SENT -> + case END -> new EndBubbleMessageItemViewHolder( DataBindingUtil.inflate( LayoutInflater.from(parent.getContext()), - R.layout.item_message_sent, + R.layout.item_message_end, parent, false)); - case RECEIVED -> + case START -> new StartBubbleMessageItemViewHolder( DataBindingUtil.inflate( LayoutInflater.from(parent.getContext()), - R.layout.item_message_received, + R.layout.item_message_start, parent, false)); default -> throw new AssertionError("Unable to create ViewHolder for type"); @@ -802,9 +796,9 @@ public class MessageAdapter extends ArrayAdapter { @NonNull @Override - public View getView(final int position, View view, final @NonNull ViewGroup parent) { + public View getView(final int position, final View view, final @NonNull ViewGroup parent) { final Message message = getItem(position); - final int type = getItemViewType(message); + final int type = getItemViewType(message, bubbleDesign.alignStart); final MessageItemViewHolder viewHolder = getViewHolder(view, parent, type); if (type == DATE_SEPARATOR @@ -822,10 +816,9 @@ public class MessageAdapter extends ArrayAdapter { return render(message, messageItemViewHolder); } - if ((type == SENT || type == RECEIVED) + if ((type == END || type == START) && viewHolder instanceof BubbleMessageItemViewHolder messageItemViewHolder) { - // TODO: type is represented by the class of viewHolder. we can get rid of that - return render(position, message, type, messageItemViewHolder); + return render(position, message, messageItemViewHolder); } throw new AssertionError(); @@ -834,7 +827,6 @@ public class MessageAdapter extends ArrayAdapter { private View render( final int position, final Message message, - final int type, final BubbleMessageItemViewHolder viewHolder) { final boolean omemoEncryption = message.getEncryption() == Message.ENCRYPTION_AXOLOTL; final boolean isInValidSession = @@ -843,8 +835,9 @@ public class MessageAdapter extends ArrayAdapter { final Account account = conversation.getAccount(); final boolean colorfulBackground = this.bubbleDesign.colorfulChatBubbles; + final boolean received = message.getStatus() == Message.STATUS_RECEIVED; final BubbleColor bubbleColor; - if (type == RECEIVED) { + if (received) { if (isInValidSession) { bubbleColor = colorfulBackground ? BubbleColor.SECONDARY : BubbleColor.SURFACE; } else { @@ -858,17 +851,20 @@ public class MessageAdapter extends ArrayAdapter { final var mergeIntoBottom = mergeIntoBottom(position, message); final var showAvatar = bubbleDesign.showAvatars - || (type == RECEIVED + || (viewHolder instanceof StartBubbleMessageItemViewHolder && message.getConversation().getMode() == Conversation.MODE_MULTI); setBubblePadding(viewHolder.root(), mergeIntoTop, mergeIntoBottom); if (showAvatar) { - final var requiresAvatar = type == SENT ? !mergeIntoBottom : !mergeIntoTop; + final var requiresAvatar = + viewHolder instanceof StartBubbleMessageItemViewHolder + ? !mergeIntoTop + : !mergeIntoBottom; setRequiresAvatar(viewHolder, requiresAvatar); AvatarWorkerTask.loadAvatar(message, viewHolder.contactPicture(), R.dimen.avatar); } else { viewHolder.contactPicture().setVisibility(View.GONE); } - setAvatarDistance(viewHolder.messageBox(), type, showAvatar); + setAvatarDistance(viewHolder.messageBox(), viewHolder.getClass(), showAvatar); viewHolder.messageBox().setClipToOutline(true); resetClickListener(viewHolder.messageBox(), viewHolder.messageBody()); @@ -998,8 +994,7 @@ public class MessageAdapter extends ArrayAdapter { setBackgroundTint(viewHolder.messageBox(), bubbleColor); setTextColor(viewHolder.messageBody(), bubbleColor); - if (type == RECEIVED - && viewHolder instanceof StartBubbleMessageItemViewHolder startViewHolder) { + if (received && viewHolder instanceof StartBubbleMessageItemViewHolder startViewHolder) { setTextColor(startViewHolder.encryption(), bubbleColor); if (isInValidSession) { startViewHolder.encryption().setVisibility(View.GONE); @@ -1019,7 +1014,10 @@ public class MessageAdapter extends ArrayAdapter { reactions -> sendReactions(message, reactions), emoji -> showDetailedReaction(message, emoji), () -> addReaction(message)); - } else if (type == SENT) { + } else { + if (viewHolder instanceof StartBubbleMessageItemViewHolder startViewHolder) { + startViewHolder.encryption().setVisibility(View.GONE); + } BindingAdapters.setReactionsOnSent( viewHolder.reactions(), message.getAggregatedReactions(), @@ -1027,7 +1025,7 @@ public class MessageAdapter extends ArrayAdapter { emoji -> showDetailedReaction(message, emoji)); } - displayStatus(viewHolder, message, type, bubbleColor); + displayStatus(viewHolder, message, bubbleColor); return viewHolder.root(); } @@ -1145,16 +1143,18 @@ public class MessageAdapter extends ArrayAdapter { } private void setAvatarDistance( - final LinearLayout messageBox, final int type, final boolean showAvatar) { + final LinearLayout messageBox, + final Class clazz, + final boolean showAvatar) { final ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) messageBox.getLayoutParams(); if (showAvatar) { final var resources = messageBox.getResources(); - if (type == RECEIVED) { + if (clazz == StartBubbleMessageItemViewHolder.class) { layoutParams.setMarginStart( resources.getDimensionPixelSize(R.dimen.bubble_avatar_distance)); layoutParams.setMarginEnd(0); - } else if (type == SENT) { + } else if (clazz == EndBubbleMessageItemViewHolder.class) { layoutParams.setMarginStart(0); layoutParams.setMarginEnd( resources.getDimensionPixelSize(R.dimen.bubble_avatar_distance)); @@ -1223,7 +1223,12 @@ public class MessageAdapter extends ArrayAdapter { } private static boolean merge(final Message a, final Message b) { - if (getItemViewType(a) != getItemViewType(b)) { + if (getItemViewType(a, false) != getItemViewType(b, false)) { + return false; + } + final var receivedA = a.getStatus() == Message.STATUS_RECEIVED; + final var receivedB = b.getStatus() == Message.STATUS_RECEIVED; + if (receivedA != receivedB) { return false; } if (a.getConversation().getMode() == Conversation.MODE_MULTI @@ -1341,6 +1346,7 @@ public class MessageAdapter extends ArrayAdapter { this.bubbleDesign = new BubbleDesign( appSettings.isColorfulChatBubbles(), + appSettings.isAlignStart(), appSettings.isLargeFont(), appSettings.isShowAvatars()); } @@ -1462,14 +1468,17 @@ public class MessageAdapter extends ArrayAdapter { private static class BubbleDesign { public final boolean colorfulChatBubbles; + public final boolean alignStart; public final boolean largeFont; public final boolean showAvatars; private BubbleDesign( final boolean colorfulChatBubbles, + final boolean alignStart, final boolean largeFont, final boolean showAvatars) { this.colorfulChatBubbles = colorfulChatBubbles; + this.alignStart = alignStart; this.largeFont = largeFont; this.showAvatars = showAvatars; } @@ -1492,7 +1501,7 @@ public class MessageAdapter extends ArrayAdapter { public abstract ConstraintLayout root(); - protected abstract ImageView editIndicator(); + protected abstract ImageView indicatorEdit(); protected abstract RelativeLayout audioPlayer(); @@ -1502,8 +1511,9 @@ public class MessageAdapter extends ArrayAdapter { protected abstract ImageView image(); - // TODO rename into indicatorSecurity() - protected abstract ImageView indicator(); + protected abstract ImageView indicatorSecurity(); + + protected abstract ImageView indicatorReceived(); protected abstract TextView time(); @@ -1516,9 +1526,9 @@ public class MessageAdapter extends ArrayAdapter { private static class StartBubbleMessageItemViewHolder extends BubbleMessageItemViewHolder { - private final ItemMessageReceivedBinding binding; + private final ItemMessageStartBinding binding; - public StartBubbleMessageItemViewHolder(@NonNull ItemMessageReceivedBinding binding) { + public StartBubbleMessageItemViewHolder(@NonNull ItemMessageStartBinding binding) { super(binding.getRoot()); this.binding = binding; } @@ -1529,7 +1539,7 @@ public class MessageAdapter extends ArrayAdapter { } @Override - protected ImageView editIndicator() { + protected ImageView indicatorEdit() { return this.binding.editIndicator; } @@ -1553,10 +1563,15 @@ public class MessageAdapter extends ArrayAdapter { return this.binding.messageContent.messageImage; } - protected ImageView indicator() { + protected ImageView indicatorSecurity() { return this.binding.securityIndicator; } + @Override + protected ImageView indicatorReceived() { + return this.binding.indicatorReceived; + } + @Override protected TextView time() { return this.binding.messageTime; @@ -1584,9 +1599,9 @@ public class MessageAdapter extends ArrayAdapter { private static class EndBubbleMessageItemViewHolder extends BubbleMessageItemViewHolder { - private final ItemMessageSentBinding binding; + private final ItemMessageEndBinding binding; - private EndBubbleMessageItemViewHolder(@NonNull ItemMessageSentBinding binding) { + private EndBubbleMessageItemViewHolder(@NonNull ItemMessageEndBinding binding) { super(binding.getRoot()); this.binding = binding; } @@ -1597,7 +1612,7 @@ public class MessageAdapter extends ArrayAdapter { } @Override - protected ImageView editIndicator() { + protected ImageView indicatorEdit() { return this.binding.editIndicator; } @@ -1622,10 +1637,11 @@ public class MessageAdapter extends ArrayAdapter { } @Override - protected ImageView indicator() { + protected ImageView indicatorSecurity() { return this.binding.securityIndicator; } + @Override protected ImageView indicatorReceived() { return this.binding.indicatorReceived; } diff --git a/src/main/res/drawable/ic_format_align_left_24dp.xml b/src/main/res/drawable/ic_format_align_left_24dp.xml new file mode 100644 index 0000000000000000000000000000000000000000..5cab3ad2c0ed7d13544039d5e55036ddadd50402 --- /dev/null +++ b/src/main/res/drawable/ic_format_align_left_24dp.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/src/main/res/layout/fragment_conversation.xml b/src/main/res/layout/fragment_conversation.xml index cfb280eb5b1b2784700a1e2ad58e08401fcd34ca..efdd10e07407280cbe512e326108205bf843183a 100644 --- a/src/main/res/layout/fragment_conversation.xml +++ b/src/main/res/layout/fragment_conversation.xml @@ -18,7 +18,7 @@ android:listSelector="@android:color/transparent" android:stackFromBottom="true" android:transcriptMode="normal" - tools:listitem="@layout/item_message_sent" /> + tools:listitem="@layout/item_message_end" /> + tools:text="10:42" /> + + - + android:src="@drawable/ic_done_24dp" + app:tint="?colorOnTertiaryContainer" /> diff --git a/src/main/res/values/defaults.xml b/src/main/res/values/defaults.xml index ca09b233ea90e935e613a3693afbc86096a31c05..eb01b23a3d1c02cdb2eae30b495dc8e09e15e749 100644 --- a/src/main/res/values/defaults.xml +++ b/src/main/res/values/defaults.xml @@ -49,4 +49,5 @@ none false true + false diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index f9d50a7aab1b1ba8014de726135f9ff979d568ae..fde70c71b2b38622e5a594693a14da759a18300e 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -1095,4 +1095,6 @@ Chat Bubbles Call integration Calls from this app interact with regular phone calls, such as ending one call when another starts. + Left-aligned messages + Display all messages, including sent ones, on the left side for a uniform chat layout. diff --git a/src/main/res/xml/preferences_interface_bubbles.xml b/src/main/res/xml/preferences_interface_bubbles.xml index c1a77967126455054f2220383c7c6723272afcd8..7c12b1b6ff29c8db6c26e3f38aa7ade3871be7b7 100644 --- a/src/main/res/xml/preferences_interface_bubbles.xml +++ b/src/main/res/xml/preferences_interface_bubbles.xml @@ -6,6 +6,11 @@ android:key="use_green_background" android:summary="@string/pref_use_colorful_bubbles_summary" android:title="@string/pref_use_colorful_bubbles" /> +