show date separators. fixes #2271

Daniel Gultsch created

Change summary

art/date_bubble_grey.svg                                            | 168 
art/date_bubble_white.svg                                           | 168 
art/render.rb                                                       |   2 
src/main/java/eu/siacs/conversations/entities/Message.java          |  12 
src/main/java/eu/siacs/conversations/ui/ConversationFragment.java   |  30 
src/main/java/eu/siacs/conversations/ui/XmppActivity.java           |   5 
src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java |  31 
src/main/java/eu/siacs/conversations/utils/UIHelper.java            |  18 
src/main/res/drawable-hdpi/date_bubble_grey.9.png                   |   0 
src/main/res/drawable-hdpi/date_bubble_white.9.png                  |   0 
src/main/res/drawable-mdpi/date_bubble_grey.9.png                   |   0 
src/main/res/drawable-mdpi/date_bubble_white.9.png                  |   0 
src/main/res/drawable-xhdpi/date_bubble_grey.9.png                  |   0 
src/main/res/drawable-xhdpi/date_bubble_white.9.png                 |   0 
src/main/res/drawable-xxhdpi/date_bubble_grey.9.png                 |   0 
src/main/res/drawable-xxhdpi/date_bubble_white.9.png                |   0 
src/main/res/drawable-xxxhdpi/date_bubble_grey.9.png                |   0 
src/main/res/drawable-xxxhdpi/date_bubble_white.9.png               |   0 
src/main/res/layout/message_date_bubble.xml                         |  26 
src/main/res/values/strings.xml                                     |   2 
20 files changed, 448 insertions(+), 14 deletions(-)

Detailed changes

art/date_bubble_grey.svg 🔗

@@ -0,0 +1,168 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="26"
+   height="26"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.92.1 r"
+   sodipodi:docname="date_bubble_white.svg">
+  <defs
+     id="defs4">
+    <filter
+       x="-0.25"
+       y="-0.25"
+       width="1.5"
+       height="1.5"
+       inkscape:label="Drop Shadow"
+       id="filter3811"
+       style="color-interpolation-filters:sRGB">
+      <feFlood
+         flood-opacity="0.25"
+         flood-color="rgb(0,0,0)"
+         result="flood"
+         id="feFlood3813" />
+      <feComposite
+         in="flood"
+         in2="SourceGraphic"
+         operator="in"
+         result="composite1"
+         id="feComposite3815" />
+      <feGaussianBlur
+         stdDeviation="0.5"
+         result="blur"
+         id="feGaussianBlur3817" />
+      <feOffset
+         dx="0"
+         dy="1"
+         result="offset"
+         id="feOffset3819" />
+      <feComposite
+         in="SourceGraphic"
+         in2="offset"
+         operator="over"
+         result="composite2"
+         id="feComposite3821" />
+    </filter>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="16"
+     inkscape:cx="9.745257"
+     inkscape:cy="9.618802"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer"
+     showgrid="true"
+     inkscape:window-width="1916"
+     inkscape:window-height="1156"
+     inkscape:window-x="0"
+     inkscape:window-y="20"
+     inkscape:window-maximized="0"
+     showguides="true"
+     inkscape:guide-bbox="true"
+     guidecolor="#000000"
+     guideopacity="0.49803922"
+     fit-margin-top="-2"
+     fit-margin-left="-2"
+     fit-margin-right="-2"
+     fit-margin-bottom="-2">
+    <inkscape:grid
+       type="xygrid"
+       id="grid2985"
+       empspacing="4"
+       visible="true"
+       enabled="true"
+       snapvisiblegridlinesonly="true"
+       spacingx="1"
+       spacingy="1"
+       originx="-9"
+       originy="-1"
+       color="#0000ff"
+       opacity="0.03137255" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="11,26"
+       id="guide3060"
+       inkscape:locked="false" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="15,26"
+       id="guide3062"
+       inkscape:locked="false" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="26,21"
+       id="guide3064"
+       inkscape:locked="false" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="26,5"
+       id="guide3066"
+       inkscape:locked="false" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="17,0"
+       id="guide3068"
+       inkscape:locked="false" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="9,0"
+       id="guide3070"
+       inkscape:locked="false" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="0,18"
+       id="guide3074"
+       inkscape:locked="false" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="0,16"
+       id="guide3076"
+       inkscape:locked="false" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer"
+     inkscape:groupmode="layer"
+     id="layer"
+     transform="translate(-9,-1)">
+    <path
+       sodipodi:nodetypes="cccc"
+       inkscape:connector-curvature="0"
+       id="path3805"
+       d="m 8,8 c 2,2 4,6 4,10 L 16,8 Z"
+       style="display:none;fill:#424242;fill-opacity:1;fill-rule:nonzero;stroke:none;filter:url(#filter3811)" />
+    <rect
+       style="fill:#424242;fill-opacity:1;fill-rule:nonzero;stroke:none;filter:url(#filter3811)"
+       id="rect2987"
+       width="20"
+       height="20"
+       x="12"
+       y="4"
+       ry="2" />
+  </g>
+</svg>

art/date_bubble_white.svg 🔗

@@ -0,0 +1,168 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="26"
+   height="26"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.92.1 r"
+   sodipodi:docname="date_bubble_white.svg">
+  <defs
+     id="defs4">
+    <filter
+       x="-0.25"
+       y="-0.25"
+       width="1.5"
+       height="1.5"
+       inkscape:label="Drop Shadow"
+       id="filter3811"
+       style="color-interpolation-filters:sRGB">
+      <feFlood
+         flood-opacity="0.25"
+         flood-color="rgb(0,0,0)"
+         result="flood"
+         id="feFlood3813" />
+      <feComposite
+         in="flood"
+         in2="SourceGraphic"
+         operator="in"
+         result="composite1"
+         id="feComposite3815" />
+      <feGaussianBlur
+         stdDeviation="0.5"
+         result="blur"
+         id="feGaussianBlur3817" />
+      <feOffset
+         dx="0"
+         dy="1"
+         result="offset"
+         id="feOffset3819" />
+      <feComposite
+         in="SourceGraphic"
+         in2="offset"
+         operator="over"
+         result="composite2"
+         id="feComposite3821" />
+    </filter>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="16"
+     inkscape:cx="9.745257"
+     inkscape:cy="9.618802"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer"
+     showgrid="true"
+     inkscape:window-width="1916"
+     inkscape:window-height="1156"
+     inkscape:window-x="0"
+     inkscape:window-y="20"
+     inkscape:window-maximized="0"
+     showguides="true"
+     inkscape:guide-bbox="true"
+     guidecolor="#000000"
+     guideopacity="0.49803922"
+     fit-margin-top="-2"
+     fit-margin-left="-2"
+     fit-margin-right="-2"
+     fit-margin-bottom="-2">
+    <inkscape:grid
+       type="xygrid"
+       id="grid2985"
+       empspacing="4"
+       visible="true"
+       enabled="true"
+       snapvisiblegridlinesonly="true"
+       spacingx="1"
+       spacingy="1"
+       originx="-9"
+       originy="-1"
+       color="#0000ff"
+       opacity="0.03137255" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="11,26"
+       id="guide3060"
+       inkscape:locked="false" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="15,26"
+       id="guide3062"
+       inkscape:locked="false" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="26,21"
+       id="guide3064"
+       inkscape:locked="false" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="26,5"
+       id="guide3066"
+       inkscape:locked="false" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="17,0"
+       id="guide3068"
+       inkscape:locked="false" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="9,0"
+       id="guide3070"
+       inkscape:locked="false" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="0,18"
+       id="guide3074"
+       inkscape:locked="false" />
+    <sodipodi:guide
+       orientation="0,1"
+       position="0,16"
+       id="guide3076"
+       inkscape:locked="false" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer"
+     inkscape:groupmode="layer"
+     id="layer"
+     transform="translate(-9,-1)">
+    <path
+       sodipodi:nodetypes="cccc"
+       inkscape:connector-curvature="0"
+       id="path3805"
+       d="m 8,8 c 2,2 4,6 4,10 L 16,8 Z"
+       style="display:none;fill:#fafafa;fill-opacity:1;fill-rule:nonzero;stroke:none;filter:url(#filter3811)" />
+    <rect
+       style="fill:#fafafa;fill-opacity:1;fill-rule:nonzero;stroke:none;filter:url(#filter3811)"
+       id="rect2987"
+       width="20"
+       height="20"
+       x="12"
+       y="4"
+       ry="2" />
+  </g>
+</svg>

art/render.rb 🔗

@@ -64,6 +64,8 @@ images = {
 	'message_bubble_received_white.svg' => ['message_bubble_received_white.9', 0],
 	'message_bubble_sent.svg' => ['message_bubble_sent.9', 0],
 	'message_bubble_sent_grey.svg' => ['message_bubble_sent_grey.9', 0],
+	'date_bubble_white.svg' => ['date_bubble_white.9', 0],
+	'date_bubble_grey.svg' => ['date_bubble_grey.9', 0],
 	}
 
 # Executable paths for Mac OSX

src/main/java/eu/siacs/conversations/entities/Message.java 🔗

@@ -10,6 +10,7 @@ import java.net.URL;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
 import eu.siacs.conversations.http.AesGcmURLStreamHandler;
+import eu.siacs.conversations.ui.adapter.MessageAdapter;
 import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.GeoHelper;
 import eu.siacs.conversations.utils.MimeUtils;
@@ -203,6 +204,14 @@ public class Message extends AbstractEntity {
 		return message;
 	}
 
+	public static Message createDateSeparator(Message message) {
+		final Message separator = new Message(message.getConversation());
+		separator.setType(Message.TYPE_STATUS);
+		separator.setBody(MessageAdapter.DATE_SEPARATOR_BODY);
+		separator.setTime(message.getTimeSent());
+		return separator;
+	}
+
 	@Override
 	public ContentValues getContentValues() {
 		ContentValues values = new ContentValues();
@@ -493,7 +502,8 @@ public class Message extends AbstractEntity {
 						!this.getBody().startsWith(ME_COMMAND) &&
 						!this.bodyIsHeart() &&
 						!message.bodyIsHeart() &&
-						((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint()))
+						((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
+						UIHelper.sameDay(message.getTimeSent(),this.getTimeSent())
 				);
 	}
 

src/main/java/eu/siacs/conversations/ui/ConversationFragment.java 🔗

@@ -46,6 +46,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.ListIterator;
 import java.util.UUID;
 import java.util.concurrent.atomic.AtomicBoolean;
 
@@ -154,14 +155,16 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
 								@Override
 								public void run() {
 									final int oldPosition = messagesView.getFirstVisiblePosition();
-									final Message message;
-									if (oldPosition < messageList.size()) {
-										message = messageList.get(oldPosition);
-									}  else {
-										message = null;
+									Message message = null;
+									int childPos;
+									for(childPos = 0; childPos + oldPosition < messageList.size(); ++childPos) {
+										message =  messageList.get(oldPosition + childPos);
+										if (message.getType() != Message.TYPE_STATUS) {
+											break;
+										}
 									}
-									String uuid = message != null ? message.getUuid() : null;
-									View v = messagesView.getChildAt(0);
+									final String uuid = message != null ? message.getUuid() : null;
+									View v = messagesView.getChildAt(childPos);
 									final int pxOffset = (v == null) ? 0 : v.getTop();
 									ConversationFragment.this.conversation.populateWithMessages(ConversationFragment.this.messageList);
 									try {
@@ -1300,7 +1303,20 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
 		this.mSendButton.setImageResource(getSendButtonImageResource(action, status));
 	}
 
+	protected void updateDateSeparators() {
+		synchronized (this.messageList) {
+			for(int i = 0; i < this.messageList.size(); ++i) {
+				final Message current = this.messageList.get(i);
+				if (i == 0 || !UIHelper.sameDay(this.messageList.get(i-1).getTimeSent(),current.getTimeSent())) {
+					this.messageList.add(i,Message.createDateSeparator(current));
+					i++;
+				}
+			}
+		}
+	}
+
 	protected void updateStatusMessages() {
+		updateDateSeparators();
 		synchronized (this.messageList) {
 			if (showLoadMoreMessages(conversation)) {
 				this.messageList.add(0, Message.createLoadMoreMessage(conversation));

src/main/java/eu/siacs/conversations/ui/XmppActivity.java 🔗

@@ -391,6 +391,7 @@ public abstract class XmppActivity extends Activity {
 		mPrimaryBackgroundColor = ContextCompat.getColor(this, R.color.grey50);
 		mSecondaryBackgroundColor = ContextCompat.getColor(this, R.color.grey200);
 
+		this.mTheme = findTheme();
 		if(isDarkTheme()) {
 			mPrimaryTextColor = ContextCompat.getColor(this, R.color.white);
 			mSecondaryTextColor = ContextCompat.getColor(this, R.color.white70);
@@ -398,8 +399,6 @@ public abstract class XmppActivity extends Activity {
 			mPrimaryBackgroundColor = ContextCompat.getColor(this, R.color.grey800);
 			mSecondaryBackgroundColor = ContextCompat.getColor(this, R.color.grey900);
 		}
-
-		this.mTheme = findTheme();
 		setTheme(this.mTheme);
 
 		this.mUsingEnterKey = usingEnterKey();
@@ -411,7 +410,7 @@ public abstract class XmppActivity extends Activity {
 	}
 
 	public boolean isDarkTheme() {
-		return getPreferences().getString("theme", getResources().getString(R.string.theme)).equals("dark");
+		return this.mTheme == R.style.ConversationsTheme_Dark || this.mTheme == R.style.ConversationsTheme_Dark_LargerText;
 	}
 
 	public int getThemeResource(int r_attr_name, int r_drawable_def) {

src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java 🔗

@@ -16,6 +16,7 @@ import android.text.Spannable;
 import android.text.SpannableString;
 import android.text.SpannableStringBuilder;
 import android.text.Spanned;
+import android.text.format.DateUtils;
 import android.text.style.ForegroundColorSpan;
 import android.text.style.RelativeSizeSpan;
 import android.text.style.StyleSpan;
@@ -72,6 +73,10 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
 	private static final int SENT = 0;
 	private static final int RECEIVED = 1;
 	private static final int STATUS = 2;
+	private static final int DATE_SEPARATOR = 3;
+
+	public static final String DATE_SEPARATOR_BODY = "DATE_SEPARATOR";
+
 	private static final Pattern XMPP_PATTERN = Pattern
 			.compile("xmpp\\:(?:(?:["
 					+ Patterns.GOOD_IRI_CHAR
@@ -135,12 +140,16 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
 
 	@Override
 	public int getViewTypeCount() {
-		return 3;
+		return 4;
 	}
 
 	public int getItemViewType(Message message) {
 		if (message.getType() == Message.TYPE_STATUS) {
-			return STATUS;
+			if (DATE_SEPARATOR_BODY.equals(message.getBody())) {
+				return DATE_SEPARATOR;
+			} else {
+				return STATUS;
+			}
 		} else if (message.getStatus() <= Message.STATUS_RECEIVED) {
 			return RECEIVED;
 		}
@@ -591,6 +600,11 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
 		if (view == null) {
 			viewHolder = new ViewHolder();
 			switch (type) {
+				case DATE_SEPARATOR:
+					view = activity.getLayoutInflater().inflate(R.layout.message_date_bubble, parent, false);
+					viewHolder.status_message = (TextView) view.findViewById(R.id.message_body);
+					viewHolder.message_box = (LinearLayout) view.findViewById(R.id.message_box);
+					break;
 				case SENT:
 					view = activity.getLayoutInflater().inflate(
 							R.layout.message_sent, parent, false);
@@ -659,7 +673,18 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
 
 		boolean darkBackground = type == RECEIVED && (!isInValidSession || mUseGreenBackground) || activity.isDarkTheme();
 
-		if (type == STATUS) {
+		if (type == DATE_SEPARATOR) {
+			if (UIHelper.today(message.getTimeSent())) {
+				viewHolder.status_message.setText(R.string.today);
+			} else if (UIHelper.yesterday(message.getTimeSent())) {
+				viewHolder.status_message.setText(R.string.yesterday);
+			} else {
+				viewHolder.status_message.setText(DateUtils.formatDateTime(activity,message.getTimeSent(),DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR));
+			}
+			viewHolder.message_box.setBackgroundResource(activity.isDarkTheme() ? R.drawable.date_bubble_grey : R.drawable.date_bubble_white);
+			viewHolder.status_message.setTextColor(activity.getSecondaryTextColor());
+			return view;
+		} else if (type == STATUS) {
 			if ("LOAD_MORE".equals(message.getBody())) {
 				viewHolder.status_message.setVisibility(View.GONE);
 				viewHolder.contact_picture.setVisibility(View.GONE);

src/main/java/eu/siacs/conversations/utils/UIHelper.java 🔗

@@ -100,6 +100,24 @@ public class UIHelper {
 		return sameDay(date,new Date(System.currentTimeMillis()));
 	}
 
+	public static boolean today(long date) {
+		return sameDay(date,System.currentTimeMillis());
+	}
+
+	public static boolean yesterday(long date) {
+		Calendar cal1 = Calendar.getInstance();
+		Calendar cal2 = Calendar.getInstance();
+		cal1.add(Calendar.DAY_OF_YEAR,-1);
+		cal2.setTime(new Date(date));
+		return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR)
+				&& cal1.get(Calendar.DAY_OF_YEAR) == cal2
+				.get(Calendar.DAY_OF_YEAR);
+	}
+
+	public static boolean sameDay(long a, long b) {
+		return sameDay(new Date(a),new Date(b));
+	}
+
 	private static boolean sameDay(Date a, Date b) {
 		Calendar cal1 = Calendar.getInstance();
 		Calendar cal2 = Calendar.getInstance();

src/main/res/layout/message_date_bubble.xml 🔗

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:paddingBottom="5dp"
+    android:paddingLeft="8dp"
+    android:paddingRight="8dp"
+    android:paddingTop="5dp">
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:background="@drawable/date_bubble_white"
+        android:id="@+id/message_box"
+        android:layout_centerHorizontal="true">
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textColorLink="@color/black87"
+            android:textColor="?attr/color_text_secondary"
+            android:textSize="?attr/TextSizeBody"
+            android:id="@+id/message_body" />
+    </LinearLayout>
+
+</RelativeLayout>

src/main/res/values/strings.xml 🔗

@@ -754,4 +754,6 @@
 	<string name="application_found_to_open_website">No application found to open website</string>
 	<string name="pref_headsup_notifications">Heads-up Notifications</string>
 	<string name="pref_headsup_notifications_summary">Show Heads-up Notifications</string>
+	<string name="today">Today</string>
+	<string name="yesterday">Yesterday</string>
 </resources>