From fcbda4c1b5e58e98ab3c1fc469201e3dc15976e4 Mon Sep 17 00:00:00 2001 From: or-else Date: Sun, 27 Dec 2020 10:15:35 -0800 Subject: [PATCH] adding feature tinode/chat/issues/443 item 10 --- .../java/co/tinode/tindroid/ChatsAdapter.java | 8 +- .../java/co/tinode/tindroid/db/MessageDb.java | 14 ++ .../java/co/tinode/tindroid/db/SqlStore.java | 11 + .../tindroid/media/PreviewFormatter.java | 214 +++++++++++++++++- .../tinode/tindroid/media/SpanFormatter.java | 150 ++++-------- .../tinode/tindroid/media/StyledTreeNode.java | 75 ++++++ app/src/main/res/drawable/ic_form.xml | 9 + app/src/main/res/values-de/strings.xml | 6 +- app/src/main/res/values-es/strings.xml | 6 +- app/src/main/res/values-ko/strings.xml | 6 +- app/src/main/res/values-ru/strings.xml | 6 +- app/src/main/res/values-zh/strings.xml | 6 +- app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/strings.xml | 4 + .../java/co/tinode/tinodesdk/Storage.java | 6 +- .../main/java/co/tinode/tinodesdk/Tinode.java | 19 +- .../co/tinode/tinodesdk/model/Drafty.java | 82 ++++++- .../java/co/tinode/tinodesdk/model/Pair.java | 11 + 18 files changed, 491 insertions(+), 143 deletions(-) create mode 100644 app/src/main/java/co/tinode/tindroid/media/StyledTreeNode.java create mode 100644 app/src/main/res/drawable/ic_form.xml create mode 100644 tinodesdk/src/main/java/co/tinode/tinodesdk/model/Pair.java diff --git a/app/src/main/java/co/tinode/tindroid/ChatsAdapter.java b/app/src/main/java/co/tinode/tindroid/ChatsAdapter.java index 07294809..4706795b 100644 --- a/app/src/main/java/co/tinode/tindroid/ChatsAdapter.java +++ b/app/src/main/java/co/tinode/tindroid/ChatsAdapter.java @@ -67,12 +67,10 @@ void resetContent(Activity activity, final boolean archive, final boolean banned newTopicIndex.put(t.getName(), newTopicIndex.size()); } - activity.runOnUiThread(() -> { - mTopics = new ArrayList<>(newTopics); - mTopicIndex = newTopicIndex; + mTopics = new ArrayList<>(newTopics); + mTopicIndex = newTopicIndex; - notifyDataSetChanged(); - }); + activity.runOnUiThread(this::notifyDataSetChanged); } @NonNull diff --git a/app/src/main/java/co/tinode/tindroid/db/MessageDb.java b/app/src/main/java/co/tinode/tindroid/db/MessageDb.java index d6887465..d5d0a17b 100644 --- a/app/src/main/java/co/tinode/tindroid/db/MessageDb.java +++ b/app/src/main/java/co/tinode/tindroid/db/MessageDb.java @@ -255,6 +255,20 @@ static Cursor getMessageById(SQLiteDatabase db, long msgId) { return db.rawQuery(sql, null); } + /* + * Get a list of the latest message for every topic, sent or received. + * See explanation here: https://stackoverflow.com/a/2111420 + */ + static Cursor getLatestMessages(SQLiteDatabase db) { + final String sql = "SELECT m1.* FROM " + TABLE_NAME + " AS m1" + + " LEFT JOIN " + TABLE_NAME + " AS m2" + + " ON (m1." + COLUMN_NAME_TOPIC_ID + "=m2." + COLUMN_NAME_TOPIC_ID + + " AND m1." + COLUMN_NAME_SEQ + " & Closeable> R getQueuedMessages(Topic topic return (R) list; } + @SuppressWarnings("unchecked") + @Override + public & Closeable> R getLatestMessages() { + MessageList list = null; + Cursor c = MessageDb.getLatestMessages(mDbh.getReadableDatabase()); + if (c != null) { + list = new MessageList(c); + } + return (R) list; + } + @Override public MsgRange[] getQueuedMessageDeletes(Topic topic, boolean hard) { StoredTopic st = (StoredTopic) topic.getLocal(); diff --git a/app/src/main/java/co/tinode/tindroid/media/PreviewFormatter.java b/app/src/main/java/co/tinode/tindroid/media/PreviewFormatter.java index eeb53223..fbd5eed4 100644 --- a/app/src/main/java/co/tinode/tindroid/media/PreviewFormatter.java +++ b/app/src/main/java/co/tinode/tindroid/media/PreviewFormatter.java @@ -1,24 +1,220 @@ package co.tinode.tindroid.media; +import android.content.Context; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.text.SpannableStringBuilder; import android.text.Spanned; +import android.text.SpannedString; +import android.text.style.CharacterStyle; +import android.text.style.ForegroundColorSpan; +import android.text.style.ImageSpan; +import android.text.style.StrikethroughSpan; +import android.text.style.StyleSpan; +import android.text.style.TypefaceSpan; +import android.util.Log; import android.widget.TextView; +import java.util.Map; + +import androidx.annotation.DrawableRes; +import androidx.annotation.StringRes; +import androidx.appcompat.content.res.AppCompatResources; +import co.tinode.tindroid.R; import co.tinode.tinodesdk.model.Drafty; // Drafty formatter for creating one-line message previews. -public class PreviewFormatter extends SpanFormatter { - private final int mLength; +public class PreviewFormatter extends AbstractDraftyFormatter { + private final float mFontSize; + + PreviewFormatter(final TextView container) { + super(container); + + mFontSize = container.getTextSize(); + } + + public static Spanned toSpanned(final TextView container, final Drafty content, final int maxLength) { + if (content == null) { + return new SpannedString(""); + } + if (content.isPlain()) { + String text = content.toString(); + if (text.length() > maxLength) { + text = text.substring(0, maxLength) + "…"; + } + return new SpannedString(text); + } + + AbstractDraftyFormatter.TreeNode result = content.format(new PreviewFormatter(container)); + if (result instanceof MeasuredTreeNode) { + try { + return ((MeasuredTreeNode) result).toSpanned(maxLength); + } catch (LengthExceededException ex) { + return new SpannableStringBuilder(ex.tail).append("…"); + } + } + + return new SpannedString(""); + } + + @Override + protected MeasuredTreeNode handleStrong(Object content) { + return new MeasuredTreeNode(new StyleSpan(Typeface.BOLD), content); + } + + @Override + protected MeasuredTreeNode handleEmphasized(Object content) { + return new MeasuredTreeNode(new StyleSpan(Typeface.ITALIC), content); + } + + @Override + protected MeasuredTreeNode handleDeleted(Object content) { + return new MeasuredTreeNode(new StrikethroughSpan(), content); + } + + @Override + protected MeasuredTreeNode handleCode(Object content) { + return new MeasuredTreeNode(new TypefaceSpan("monospace"), content); + } + + @Override + protected MeasuredTreeNode handleHidden(Object content) { + return null; + } + + @Override + protected MeasuredTreeNode handleLineBreak() { + return null; + } + + @Override + protected MeasuredTreeNode handleLink(Context ctx, Object content, Map data) { + return new MeasuredTreeNode(new ForegroundColorSpan(ctx.getResources().getColor(R.color.colorAccent)), content); + } + + @Override + protected MeasuredTreeNode handleMention(Context ctx, Object content, Map data) { + return new MeasuredTreeNode(content); + } - PreviewFormatter(final TextView container, final int length) { - super(container, null); - mLength = length; + @Override + protected MeasuredTreeNode handleHashtag(Context ctx, Object content, Map data) { + return new MeasuredTreeNode(content); } - public static Spanned toSpanned(final TextView container, final Drafty content) { - return toSpanned(container, content, null); + private MeasuredTreeNode annotatedIcon(Context ctx, @DrawableRes int iconId, @StringRes int stringId) { + MeasuredTreeNode node = null; + Drawable icon = AppCompatResources.getDrawable(ctx, iconId); + if (icon != null) { + icon.setTint(ctx.getResources().getColor(R.color.colorDarkGray)); + icon.setBounds(0, 0, (int) mFontSize, (int) mFontSize); + node = new MeasuredTreeNode(); + node.addNode(new ImageSpan(icon, ImageSpan.ALIGN_BOTTOM), " "); + node.addNode(" " + ctx.getResources().getString(stringId)); + } + return node; + } + + @Override + protected MeasuredTreeNode handleImage(Context ctx, Object content, Map data) { + return annotatedIcon(ctx, R.drawable.ic_image, R.string.picture); + } + + @Override + protected MeasuredTreeNode handleAttachment(Context ctx, Map data) { + return annotatedIcon(ctx, R.drawable.ic_attach, R.string.attachment); + } + + @Override + protected MeasuredTreeNode handleButton(Context ctx, Map data, Object content) { + MeasuredTreeNode node = new MeasuredTreeNode("["); + node.addNode(new MeasuredTreeNode(content)); + node.addNode(new MeasuredTreeNode("]")); + return node; + } + + @Override + protected MeasuredTreeNode handleFormRow(Context ctx, Map data, Object content) { + return new MeasuredTreeNode(content); + } + + @Override + protected MeasuredTreeNode handleForm(Context ctx, Map data, Object content) { + MeasuredTreeNode node = annotatedIcon(ctx, R.drawable.ic_form, R.string.form); + node.addNode(new MeasuredTreeNode(": ")); + node.addNode(new MeasuredTreeNode(content)); + return node; + } + + @Override + protected MeasuredTreeNode handleUnknown(Context ctx, Object content, Map data) { + return annotatedIcon(ctx, R.drawable.ic_unkn_type, R.string.unknown); + } + + static class MeasuredTreeNode extends StyledTreeNode { + private static final String TAG = "MeasuredTreeNode"; + + MeasuredTreeNode() { + super(); + } + + MeasuredTreeNode(CharSequence content) { + super(content); + } + + MeasuredTreeNode(Object content) { + super(content); + } + + MeasuredTreeNode(CharacterStyle style, Object content) { + super(style, content); + } + + protected Spanned toSpanned(final int maxLength) { + SpannableStringBuilder spanned = new SpannableStringBuilder(); + boolean exceeded = false; + + if (isPlain()) { + CharSequence text = getText(); + if (text.length() > maxLength) { + text = text.subSequence(0, maxLength); + exceeded = true; + } + spanned.append(text); + } else if (hasChildren()) { + try { + for (AbstractDraftyFormatter.TreeNode child : getChildren()) { + if (child == null) { + Log.w(TAG, "NULL child. Should not happen!!!"); + } else if (child instanceof MeasuredTreeNode) { + spanned.append(((MeasuredTreeNode) child).toSpanned(maxLength - spanned.length())); + } else { + Log.w(TAG, "Wrong child class: " + child.getClass().getSimpleName()); + } + } + } catch (LengthExceededException ex) { + exceeded = true; + spanned.append(ex.tail); + } + } + + if (spanned.length() > 0 && (cStyle != null || pStyle != null)) { + spanned.setSpan(cStyle != null ? cStyle : pStyle, + 0, spanned.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + if (exceeded) { + throw new LengthExceededException(spanned); + } + + return spanned; + } } - public static boolean hasClickableSpans(final Drafty content) { - return false; + static class LengthExceededException extends RuntimeException { + private final Spanned tail; + LengthExceededException(Spanned tail) { + this.tail = tail; + } } } diff --git a/app/src/main/java/co/tinode/tindroid/media/SpanFormatter.java b/app/src/main/java/co/tinode/tindroid/media/SpanFormatter.java index 3cb6791f..9fd28672 100644 --- a/app/src/main/java/co/tinode/tindroid/media/SpanFormatter.java +++ b/app/src/main/java/co/tinode/tindroid/media/SpanFormatter.java @@ -8,7 +8,6 @@ import android.graphics.Typeface; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; -import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.SpannedString; import android.text.TextUtils; @@ -17,7 +16,6 @@ import android.text.style.ForegroundColorSpan; import android.text.style.ImageSpan; import android.text.style.LeadingMarginSpan; -import android.text.style.ParagraphStyle; import android.text.style.StrikethroughSpan; import android.text.style.StyleSpan; import android.text.style.SubscriptSpan; @@ -44,7 +42,7 @@ /** * Convert Drafty object into a Spanned object with full support for all features. */ -public class SpanFormatter extends AbstractDraftyFormatter { +public class SpanFormatter extends AbstractDraftyFormatter { private static final String TAG = "SpanFormatter"; private static final float FORM_LINE_SPACING = 1.2f; @@ -76,8 +74,8 @@ public static Spanned toSpanned(final TextView container, final Drafty content, } AbstractDraftyFormatter.TreeNode result = content.format(new SpanFormatter(container, clicker)); - if (result instanceof TreeNode) { - return ((TreeNode)result).toSpanned(); + if (result instanceof StyledTreeNode) { + return ((StyledTreeNode)result).toSpanned(); } return new SpannedString(""); @@ -127,41 +125,41 @@ protected static float scaleBitmap(int srcWidth, int srcHeight, int viewportWidt } @Override - protected TreeNode handleStrong(Object content) { - return new TreeNode(new StyleSpan(Typeface.BOLD), content); + protected StyledTreeNode handleStrong(Object content) { + return new StyledTreeNode(new StyleSpan(Typeface.BOLD), content); } @Override - protected TreeNode handleEmphasized(Object content) { - return new TreeNode(new StyleSpan(Typeface.ITALIC), content); + protected StyledTreeNode handleEmphasized(Object content) { + return new StyledTreeNode(new StyleSpan(Typeface.ITALIC), content); } @Override - protected TreeNode handleDeleted(Object content) { - return new TreeNode(new StrikethroughSpan(), content); + protected StyledTreeNode handleDeleted(Object content) { + return new StyledTreeNode(new StrikethroughSpan(), content); } @Override - protected TreeNode handleCode(Object content) { - return new TreeNode(new TypefaceSpan("monospace"), content); + protected StyledTreeNode handleCode(Object content) { + return new StyledTreeNode(new TypefaceSpan("monospace"), content); } @Override - protected TreeNode handleHidden(Object content) { + protected StyledTreeNode handleHidden(Object content) { return null; } @Override - protected TreeNode handleLineBreak() { - return new TreeNode("\n"); + protected StyledTreeNode handleLineBreak() { + return new StyledTreeNode("\n"); } @Override - protected TreeNode handleLink(Context ctx, Object content, Map data) { + protected StyledTreeNode handleLink(Context ctx, Object content, Map data) { try { // We don't need to specify an URL for URLSpan // as it's not going to be used. - return new TreeNode(new URLSpan("") { + return new StyledTreeNode(new URLSpan("") { @Override public void onClick(View widget) { if (mClicker != null) { @@ -175,22 +173,22 @@ public void onClick(View widget) { } @Override - protected TreeNode handleMention(Context ctx, Object content, Map data) { + protected StyledTreeNode handleMention(Context ctx, Object content, Map data) { return null; } @Override - protected TreeNode handleHashtag(Context ctx, Object content, Map data) { + protected StyledTreeNode handleHashtag(Context ctx, Object content, Map data) { return null; } @Override - protected TreeNode handleImage(final Context ctx, Object content, final Map data) { + protected StyledTreeNode handleImage(final Context ctx, Object content, final Map data) { if (data == null) { return null; } - TreeNode result = null; + StyledTreeNode result = null; // Bitmap dimensions specified by the sender. int width = 0, height = 0; Object tmp; @@ -301,37 +299,37 @@ protected TreeNode handleImage(final Context ctx, Object content, final Map data, Object content) { - return new TreeNode(content); + protected StyledTreeNode handleFormRow(Context ctx, Map data, Object content) { + return new StyledTreeNode(content); } @Override - protected TreeNode handleForm(Context ctx, Map data, Object content) { - TreeNode span = null; + protected StyledTreeNode handleForm(Context ctx, Map data, Object content) { + StyledTreeNode span = null; if (content instanceof List) { // Add line breaks between form elements. try { @SuppressWarnings("unchecked") List children = (List) content; if (children.size() > 0) { - span = new TreeNode(); + span = new StyledTreeNode(); for (TreeNode child : children) { span.addNode(child); span.addNode("\n"); @@ -351,7 +349,7 @@ protected TreeNode handleForm(Context ctx, Map data, Object cont } @Override - protected TreeNode handleAttachment(final Context ctx, final Map data) { + protected StyledTreeNode handleAttachment(final Context ctx, final Map data) { if (data == null) { return null; } @@ -366,13 +364,13 @@ protected TreeNode handleAttachment(final Context ctx, final Map } catch (ClassCastException ignored) { } - TreeNode result = new TreeNode(); + StyledTreeNode result = new StyledTreeNode(); // Insert document icon Drawable icon = AppCompatResources.getDrawable(ctx, R.drawable.ic_file); icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight()); ImageSpan span = new ImageSpan(icon, ImageSpan.ALIGN_BOTTOM); final Rect bounds = span.getDrawable().getBounds(); - result.addNode(new SubscriptSpan(), new TreeNode(span, " ")); + result.addNode(new SubscriptSpan(), new StyledTreeNode(span, " ")); // Insert document's file name String fname = null; @@ -393,7 +391,7 @@ protected TreeNode handleAttachment(final Context ctx, final Map // Insert linebreak then a clickable [↓ save] or [(!) unavailable] line. result.addNode("\n"); - TreeNode saveLink = new TreeNode(); + StyledTreeNode saveLink = new StyledTreeNode(); // Add 'download file' icon icon = AppCompatResources.getDrawable(ctx, valid ? R.drawable.ic_download_link : R.drawable.ic_error_gray); @@ -404,7 +402,7 @@ protected TreeNode handleAttachment(final Context ctx, final Map saveLink.addNode(new ImageSpan(icon, ImageSpan.ALIGN_BOTTOM), " "); if (valid) { // Clickable "save". - saveLink.addNode(new TreeNode(new ClickableSpan() { + saveLink.addNode(new StyledTreeNode(new ClickableSpan() { @Override public void onClick(@NonNull View widget) { mClicker.onClick("EX", data); @@ -423,7 +421,7 @@ public void onClick(@NonNull View widget) { // Button: URLSpan wrapped into LineHeightSpan and then BorderedSpan. @Override - protected TreeNode handleButton(final Context ctx, final Map data, final Object content) { + protected StyledTreeNode handleButton(final Context ctx, final Map data, final Object content) { // This is needed for button shadows. mContainer.setLayerType(View.LAYER_TYPE_SOFTWARE, null); @@ -432,7 +430,7 @@ protected TreeNode handleButton(final Context ctx, final Map dat float dipSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1.0f, metrics); // Create BorderSpan. - final TreeNode span = new TreeNode( + final StyledTreeNode span = new StyledTreeNode( (CharacterStyle) new BorderedSpan(mContainer.getContext(), mFontSize, dipSize), (Object) null); // Wrap URLSpan into BorderSpan. @@ -450,7 +448,7 @@ public void onClick(View widget) { // Unknown or unsupported element. @Override - protected TreeNode handleUnknown(final Context ctx, final Object content, final Map data) { + protected StyledTreeNode handleUnknown(final Context ctx, final Object content, final Map data) { if (data == null) { return null; } @@ -479,12 +477,12 @@ protected TreeNode handleUnknown(final Context ctx, final Object content, final scaledHeight = (int) (height * scale * metrics.density); } - TreeNode result = null; + StyledTreeNode result = null; Drawable unkn = AppCompatResources.getDrawable(ctx, R.drawable.ic_unkn_type); if (unkn != null) { unkn.setBounds(0, 0, unkn.getIntrinsicWidth(), unkn.getIntrinsicHeight()); CharacterStyle span = new ImageSpan(UiUtils.getPlaceholder(ctx, unkn, null, scaledWidth, scaledHeight)); - result = new TreeNode(span, content); + result = new StyledTreeNode(span, content); } return result; @@ -493,76 +491,4 @@ protected TreeNode handleUnknown(final Context ctx, final Object content, final public interface ClickListener { void onClick(String type, Map data); } - - // Structure representing Drafty as a tree of formatting nodes. - static class TreeNode extends AbstractDraftyFormatter.TreeNode { - private CharacterStyle cStyle; - private ParagraphStyle pStyle; - - TreeNode() { - super(); - cStyle = null; - pStyle = null; - } - - private TreeNode(CharSequence content) { - super(content); - } - - private TreeNode(Object content) { - super(content); - } - - private TreeNode(CharacterStyle style, CharSequence text) { - super(text); - this.cStyle = style; - } - - TreeNode(CharacterStyle style, Object content) { - super(content); - this.cStyle = style; - } - - TreeNode(ParagraphStyle style, Object content) { - super(content); - this.pStyle = style; - } - - void addNode(CharacterStyle style, Object content) { - if (content == null) { - return; - } - addNode(new TreeNode(style, content)); - } - - void addNode(ParagraphStyle style, Object content) { - if (content == null) { - return; - } - addNode(new TreeNode(style, content)); - } - - Spanned toSpanned() { - SpannableStringBuilder spanned = new SpannableStringBuilder(); - if (isPlain()) { - spanned.append(getText()); - } else if (hasChildren()) { - for (AbstractDraftyFormatter.TreeNode child : getChildren()) { - if (child == null) { - Log.w(TAG, "NULL child. Should not happen!!!"); - } else if (child instanceof TreeNode){ - spanned.append(((TreeNode) child).toSpanned()); - } else { - Log.w(TAG, "Wrong child class: " + child.getClass().getSimpleName()); - } - } - } - - if (spanned.length() > 0 && (cStyle != null || pStyle != null)) { - spanned.setSpan(cStyle != null ? cStyle : pStyle, - 0, spanned.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - return spanned; - } - } } diff --git a/app/src/main/java/co/tinode/tindroid/media/StyledTreeNode.java b/app/src/main/java/co/tinode/tindroid/media/StyledTreeNode.java new file mode 100644 index 00000000..5921b52b --- /dev/null +++ b/app/src/main/java/co/tinode/tindroid/media/StyledTreeNode.java @@ -0,0 +1,75 @@ +package co.tinode.tindroid.media; + +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.CharacterStyle; +import android.text.style.ParagraphStyle; +import android.util.Log; + +class StyledTreeNode extends AbstractDraftyFormatter.TreeNode { + private static final String TAG = "StyledTreeNode"; + + protected CharacterStyle cStyle; + protected ParagraphStyle pStyle; + + StyledTreeNode() { + super(); + cStyle = null; + pStyle = null; + } + + StyledTreeNode(CharSequence content) { + super(content); + } + + StyledTreeNode(Object content) { + super(content); + } + + StyledTreeNode(CharacterStyle style, Object content) { + super(content); + this.cStyle = style; + } + + StyledTreeNode(ParagraphStyle style, Object content) { + super(content); + this.pStyle = style; + } + + void addNode(CharacterStyle style, Object content) { + if (content == null) { + return; + } + addNode(new StyledTreeNode(style, content)); + } + + void addNode(ParagraphStyle style, Object content) { + if (content == null) { + return; + } + addNode(new StyledTreeNode(style, content)); + } + + protected Spanned toSpanned() { + SpannableStringBuilder spanned = new SpannableStringBuilder(); + if (isPlain()) { + spanned.append(getText()); + } else if (hasChildren()) { + for (AbstractDraftyFormatter.TreeNode child : getChildren()) { + if (child == null) { + Log.w(TAG, "NULL child. Should not happen!!!"); + } else if (child instanceof StyledTreeNode){ + spanned.append(((StyledTreeNode) child).toSpanned()); + } else { + Log.w(TAG, "Wrong child class: " + child.getClass().getSimpleName()); + } + } + } + + if (spanned.length() > 0 && (cStyle != null || pStyle != null)) { + spanned.setSpan(cStyle != null ? cStyle : pStyle, + 0, spanned.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + return spanned; + } +} diff --git a/app/src/main/res/drawable/ic_form.xml b/app/src/main/res/drawable/ic_form.xml new file mode 100644 index 00000000..fc3127a9 --- /dev/null +++ b/app/src/main/res/drawable/ic_form.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 5edba187..0475e3b8 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -273,5 +273,9 @@ Dies ist ein Sender Der Sender ermöglicht unbegrenzte Teilnehmer mit minimaler Zugriffskontrolle. Diese Wahl ist dauerhaft. Sender - unavailable + nicht verfügbar + Bild + Anhang + Unbekannt + Bilden diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 3616e87f..14de659b 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -272,5 +272,9 @@ Este es un canal El canal permite participantes ilimitados con un control de acceso mínimo. Esta elección es permanente. canal - unavailable + indisponible + Imagen + Adjunto archivo + Desconocido + Formulario diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 13f77ffb..4b5a4645 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -271,5 +271,9 @@ 이것은채널입니다 채널은 최소한의 액세스 제어로 무제한 참가자를 허용합니다. 이 선택은 영구적입니다. 채널 - unavailable + 없는 + 심상 + 첨부 파일 + 알려지지 않은 + 양식 diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 20561609..456c030f 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -252,5 +252,9 @@ Это канал Канал позволяет неограниченное число подписчиков с минимальным контролем доступа. Этот выбор окончателен. канал - unavailable + недоступно + Картинка + Вложение + Неизвестно + Форма diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 38ea1d1c..c42362ef 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -273,5 +273,9 @@ 这是一个频道 该通道允许无限的订户以最小的访问控制。 此选择是永久的。 频道 - unavailable + 不可用 + 图片 + 附属 + 未知 + 表格 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 4372b555..0b05f512 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -17,6 +17,7 @@ #CCCCCC #999999 + #757575 #CCCCCC diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1917e5d1..e9aa4407 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -286,4 +286,8 @@ Channels allow unlimited subscribers with minimal access control. This selection is permanent. channel unavailable + Picture + Attachment + Unknown + Form diff --git a/tinodesdk/src/main/java/co/tinode/tinodesdk/Storage.java b/tinodesdk/src/main/java/co/tinode/tinodesdk/Storage.java index 21f0dd69..cd2b586c 100644 --- a/tinodesdk/src/main/java/co/tinode/tinodesdk/Storage.java +++ b/tinodesdk/src/main/java/co/tinode/tinodesdk/Storage.java @@ -204,8 +204,12 @@ public interface Storage { /** Retrieve a single message by database id */ T getMessageById(Topic topic, long dbMessageId); - /** Get a list of unsent messages */ + /** Get the latest message for each topic. Close the result after use. */ + & Closeable> T getLatestMessages(); + + /** Get a list of unsent messages. Close the result after use. */ & Closeable> T getQueuedMessages(Topic topic); + /** * Get a list of pending delete message ranges. * @param topic topic where the messages were deleted. diff --git a/tinodesdk/src/main/java/co/tinode/tinodesdk/Tinode.java b/tinodesdk/src/main/java/co/tinode/tinodesdk/Tinode.java index 1124fb1d..45310457 100644 --- a/tinodesdk/src/main/java/co/tinode/tinodesdk/Tinode.java +++ b/tinodesdk/src/main/java/co/tinode/tinodesdk/Tinode.java @@ -14,6 +14,7 @@ import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.type.TypeFactory; +import java.io.Closeable; import java.io.IOException; import java.io.InvalidObjectException; import java.net.MalformedURLException; @@ -63,6 +64,7 @@ import co.tinode.tinodesdk.model.MsgServerMeta; import co.tinode.tinodesdk.model.MsgServerPres; import co.tinode.tinodesdk.model.MsgSetMeta; +import co.tinode.tinodesdk.model.Pair; import co.tinode.tinodesdk.model.PrivateType; import co.tinode.tinodesdk.model.ServerMessage; import co.tinode.tinodesdk.model.Subscription; @@ -135,7 +137,7 @@ public class Tinode { private final String mAppName; private final ListenerNotifier mNotifier; private final ConcurrentMap mFutures; - private final ConcurrentHashMap mTopics; + private final ConcurrentHashMap> mTopics; private final ConcurrentHashMap mUsers; private JavaType mDefaultTypeOfMetaPacket = null; private final MimeTypeResolver mMimeResolver = null; @@ -375,16 +377,27 @@ public void setOsString(String os) { mOsVersion = os; } - private void loadTopics() { + private & Closeable> void loadTopics() { if (mStore != null && mStore.isReady() && !mTopicsLoaded) { Topic[] topics = mStore.topicGetAll(this); if (topics != null) { for (Topic tt : topics) { tt.setStorage(mStore); - mTopics.put(tt.getName(), tt); + mTopics.put(tt.getName(), new Pair(tt, null)); setTopicsUpdated(tt.getUpdated()); } } + // Load last message for each topic. + ML latest = mStore.getLatestMessages(); + if (latest != null) { + while (latest.hasNext()) { + Storage.Message msg = latest.next(); + msg. + } + try { + latest.close(); + } catch (IOException ignored) {} + } mTopicsLoaded = true; } } diff --git a/tinodesdk/src/main/java/co/tinode/tinodesdk/model/Drafty.java b/tinodesdk/src/main/java/co/tinode/tinodesdk/model/Drafty.java index d5008317..8e095b8c 100644 --- a/tinodesdk/src/main/java/co/tinode/tinodesdk/model/Drafty.java +++ b/tinodesdk/src/main/java/co/tinode/tinodesdk/model/Drafty.java @@ -6,7 +6,6 @@ import java.io.Serializable; import java.net.URI; -import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -668,13 +667,16 @@ public Drafty append(Drafty that) { } } - for (int i=0; i key) { style.key = ent_idx; - ent[ent_idx ++] = that.ent[that.fmt[i].key]; + ent[ent_idx ++] = that.ent[key]; + } else { + continue; } fmt[fmt_idx ++] = style; } @@ -738,7 +740,7 @@ protected Drafty insertButton(int at, int len, String name, String actionType, S } /** - * Check if the give Drafty can be represented by plain text. + * Check if the given Drafty can be represented by plain text. * * @return true if this Drafty has no markup other thn line breaks. */ @@ -913,6 +915,65 @@ public String toPlainText() { "ent: " + Arrays.toString(ent) + "}"; } + /** + * Shorten Drafty document and strip all entity data leaving just inline styles and entity references. + * @param length length in characters to shorten to. + * @return new shortened Drafty object leaving the original intact. + */ + public Drafty preview(final int length) { + Drafty preview = new Drafty(); + + if (txt != null) { + if (txt.length() > length) { + preview.txt = txt.substring(0, length); + } else { + preview.txt = txt; + } + } + + int len = preview.txt != null ? preview.txt.length() : 0; + if (fmt != null && fmt.length > 0) { + // Count styles and entities which cover just the new length of the text. + int fmt_count = 0, ent_count = 0; + for (Style st : fmt) { + if (st.at < len) { + fmt_count ++; + if (st.key != null) { + ent_count ++; + } + } + } + + // Allocate space for copying styles and entities. + preview.fmt = new Style[fmt_count]; + if (ent != null && ent_count > 0) { + preview.ent = new Entity[ent_count]; + } + + // Insertion point for styles. + int fmt_idx = 0; + // Insertion point for entities. + int ent_idx = 0; + for (Style st : fmt) { + if (st.at < len) { + Style style = new Style(null, st.at, st.len); + int key = st.key != null ? st.key : 0; + if (st.tp != null && !st.tp.equals("")) { + style.tp = st.tp; + } else if (ent != null && ent.length > key) { + style.key = ent_idx; + preview.ent[ent_idx++] = ent[key].copyLight(); + } else { + continue; + } + fmt[fmt_idx++] = style; + } + } + } + + return preview; + } + public static class Style implements Serializable, Comparable