fixed conversation display

This commit is contained in:
Mariotaku Lee 2017-02-12 10:39:47 +08:00
parent 6284e575e9
commit f33ecab166
No known key found for this signature in database
GPG Key ID: 15C10F89D7C33535
12 changed files with 367 additions and 61 deletions

View File

@ -116,9 +116,31 @@ public class DMResponse implements Parcelable {
@JsonObject
public static class Entry implements Parcelable {
@JsonField(name = "conversation_create")
Message conversationCreate;
@JsonField(name = "join_conversation")
Message joinConversation;
@JsonField(name = "message")
Message message;
public Message getJoinConversation() {
return joinConversation;
}
public Message getConversationCreate() {
return conversationCreate;
}
public Message getMessage() {
return message;
}
@Override
public String toString() {
return "Entry{" +
"message=" + message +
'}';
}
@ParcelablePlease
@JsonObject
@ -130,9 +152,25 @@ public class DMResponse implements Parcelable {
@JsonField(name = "time")
long time;
@JsonField(name = "affects_sort")
boolean affectsSort;
@JsonField(name = "conversation_id")
String conversationId;
@JsonField(name = "request_id")
String requestId;
@JsonField(name = "message_data")
Data messageData;
@JsonField(name = "participants")
Conversation.Participant[] participants;
public boolean isAffectsSort() {
return affectsSort;
}
public String getConversationId() {
return conversationId;
}
@ -145,8 +183,56 @@ public class DMResponse implements Parcelable {
return time;
}
public String getRequestId() {
return requestId;
}
public Data getMessageData() {
return messageData;
}
public Conversation.Participant[] getParticipants() {
return participants;
}
@Override
public String toString() {
return "Message{" +
"affectsSort=" + affectsSort +
", id=" + id +
", time=" + time +
", conversationId='" + conversationId + '\'' +
", messageData=" + messageData +
'}';
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
DMResponse$Entry$MessageParcelablePlease.writeToParcel(this, dest, flags);
}
public static final Creator<Message> CREATOR = new Creator<Message>() {
@Override
public Message createFromParcel(Parcel source) {
Message target = new Message();
DMResponse$Entry$MessageParcelablePlease.readFromParcel(target, source);
return target;
}
@Override
public Message[] newArray(int size) {
return new Message[size];
}
};
@ParcelablePlease
@JsonObject
public static class Data implements EntitySupport {
public static class Data implements EntitySupport, Parcelable {
@JsonField(name = "id")
long id;
@ -216,40 +302,68 @@ public class DMResponse implements Parcelable {
return id;
}
@ParcelablePlease
@JsonObject
public static class Attachment {
public static class Attachment implements Parcelable {
@JsonField(name = "photo")
MediaEntity photo;
public MediaEntity getPhoto() {
return photo;
}
}
}
@Override
public int describeContents() {
return 0;
}
@Override
public String toString() {
return "Attachment{" +
"photo=" + photo +
'}';
}
@Override
public void writeToParcel(Parcel dest, int flags) {
DMResponse$Entry$MessageParcelablePlease.writeToParcel(this, dest, flags);
}
@Override
public int describeContents() {
return 0;
}
public static final Creator<Message> CREATOR = new Creator<Message>() {
@Override
public Message createFromParcel(Parcel source) {
Message target = new Message();
DMResponse$Entry$MessageParcelablePlease.readFromParcel(target, source);
return target;
@Override
public void writeToParcel(Parcel dest, int flags) {
DMResponse$Entry$Message$Data$AttachmentParcelablePlease.writeToParcel(this, dest, flags);
}
public static final Creator<Attachment> CREATOR = new Creator<Attachment>() {
public Attachment createFromParcel(Parcel source) {
Attachment target = new Attachment();
DMResponse$Entry$Message$Data$AttachmentParcelablePlease.readFromParcel(target, source);
return target;
}
public Attachment[] newArray(int size) {
return new Attachment[size];
}
};
}
@Override
public Message[] newArray(int size) {
return new Message[size];
public int describeContents() {
return 0;
}
};
@Override
public void writeToParcel(Parcel dest, int flags) {
DMResponse$Entry$Message$DataParcelablePlease.writeToParcel(this, dest, flags);
}
public static final Creator<Data> CREATOR = new Creator<Data>() {
public Data createFromParcel(Parcel source) {
Data target = new Data();
DMResponse$Entry$Message$DataParcelablePlease.readFromParcel(target, source);
return target;
}
public Data[] newArray(int size) {
return new Data[size];
}
};
}
}
@Override

View File

@ -153,6 +153,7 @@ public class ParcelableMessage {
@StringDef({MessageType.TEXT, MessageType.STICKER})
public @interface MessageType {
String CONVERSATION_CREATE = "conversation_create";
String TEXT = "text";
String STICKER = "sticker";
}

View File

@ -10,6 +10,7 @@ import org.apache.commons.lang3.text.translate.CharSequenceTranslator;
import org.apache.commons.lang3.text.translate.EntityArrays;
import org.apache.commons.lang3.text.translate.LookupTranslator;
import org.mariotaku.commons.text.CodePointArray;
import org.mariotaku.microblog.library.twitter.model.DMResponse;
import org.mariotaku.microblog.library.twitter.model.DirectMessage;
import org.mariotaku.microblog.library.twitter.model.EntitySupport;
import org.mariotaku.microblog.library.twitter.model.ExtendedEntitySupport;
@ -42,20 +43,20 @@ public class InternalTwitterContentUtils {
}
public static boolean isFiltered(final SQLiteDatabase database, final UserKey userKey,
final String textPlain, final String quotedTextPlain,
final SpanItem[] spans, final SpanItem[] quotedSpans,
final String source, final String quotedSource,
final UserKey retweetedById, final UserKey quotedUserId) {
final String textPlain, final String quotedTextPlain,
final SpanItem[] spans, final SpanItem[] quotedSpans,
final String source, final String quotedSource,
final UserKey retweetedById, final UserKey quotedUserId) {
return isFiltered(database, userKey, textPlain, quotedTextPlain, spans, quotedSpans, source,
quotedSource, retweetedById, quotedUserId, true);
}
public static boolean isFiltered(final SQLiteDatabase database, final UserKey userKey,
final String textPlain, final String quotedTextPlain,
final SpanItem[] spans, final SpanItem[] quotedSpans,
final String source, final String quotedSource,
final UserKey retweetedByKey, final UserKey quotedUserKey,
final boolean filterRts) {
final String textPlain, final String quotedTextPlain,
final SpanItem[] spans, final SpanItem[] quotedSpans,
final String source, final String quotedSource,
final UserKey retweetedByKey, final UserKey quotedUserKey,
final boolean filterRts) {
if (database == null) return false;
if (textPlain == null && spans == null && userKey == null && source == null)
return false;
@ -163,7 +164,7 @@ public class InternalTwitterContentUtils {
}
public static boolean isFiltered(final SQLiteDatabase database, final ParcelableStatus status,
final boolean filterRTs) {
final boolean filterRTs) {
if (database == null || status == null) return false;
return isFiltered(database, status.user_key, status.text_plain, status.quoted_text_plain,
status.spans, status.quoted_spans, status.source, status.quoted_source,
@ -223,6 +224,13 @@ public class InternalTwitterContentUtils {
return builder.buildWithIndices();
}
@NonNull
public static Pair<String, SpanItem[]> formatDirectMessageText(@NonNull final DMResponse.Entry.Message.Data message) {
final HtmlBuilder builder = new HtmlBuilder(message.getText(), false, true, false);
parseEntities(builder, message);
return builder.buildWithIndices();
}
@NonNull
public static StatusTextWithIndices formatStatusTextWithIndices(@NonNull final Status status) {
//TODO handle twitter video url

View File

@ -35,6 +35,7 @@ import org.mariotaku.twidere.model.ParcelableMessage
import org.mariotaku.twidere.model.ParcelableMessage.MessageType
import org.mariotaku.twidere.util.TwidereLinkify
import org.mariotaku.twidere.view.holder.message.AbsMessageViewHolder
import org.mariotaku.twidere.view.holder.message.ConversationCreateMessageViewHolder
import org.mariotaku.twidere.view.holder.message.MessageViewHolder
import org.mariotaku.twidere.view.holder.message.StickerMessageViewHolder
import java.util.*
@ -66,13 +67,17 @@ class MessagesConversationAdapter(context: Context) : LoadMoreSupportAdapter<Rec
val view = inflater.inflate(StickerMessageViewHolder.layoutResource, parent, false)
return StickerMessageViewHolder(view, this)
}
ITEM_TYPE_CONVERSATION_CREATE -> {
val view = inflater.inflate(ConversationCreateMessageViewHolder.layoutResource, parent, false)
return ConversationCreateMessageViewHolder(view, this)
}
}
throw UnsupportedOperationException()
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder.itemViewType) {
ITEM_TYPE_TEXT_MESSAGE, ITEM_TYPE_STICKER_MESSAGE -> {
ITEM_TYPE_TEXT_MESSAGE, ITEM_TYPE_STICKER_MESSAGE, ITEM_TYPE_CONVERSATION_CREATE -> {
val message = getMessage(position)!!
// Display date for oldest item
var showDate = true
@ -105,6 +110,9 @@ class MessagesConversationAdapter(context: Context) : LoadMoreSupportAdapter<Rec
MessageType.STICKER -> {
return ITEM_TYPE_STICKER_MESSAGE
}
MessageType.CONVERSATION_CREATE -> {
return ITEM_TYPE_CONVERSATION_CREATE
}
else -> return ITEM_TYPE_TEXT_MESSAGE
}
}
@ -117,6 +125,7 @@ class MessagesConversationAdapter(context: Context) : LoadMoreSupportAdapter<Rec
const val ITEM_TYPE_TEXT_MESSAGE = 1
const val ITEM_TYPE_STICKER_MESSAGE = 2
const val ITEM_TYPE_CONVERSATION_CREATE = 3
}
}

View File

@ -157,7 +157,7 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
adapter.getData(), true, loadingMore)
// Setting comparator to null lets statuses sort ascending
loader.comparator = Comparator { l, r ->
(l.sort_id - r.sort_id).toInt()
(r.sort_id - l.sort_id).toInt()
}
return loader
}
@ -365,8 +365,8 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
}
override fun handleKeyboardShortcutSingle(handler: KeyboardShortcutsHandler,
keyCode: Int, event: KeyEvent,
metaState: Int): Boolean {
keyCode: Int, event: KeyEvent,
metaState: Int): Boolean {
if (!KeyboardShortcutsHandler.isValidForHotkey(keyCode, event)) return false
val focusedChild = RecyclerViewUtils.findRecyclerViewChild(recyclerView, layoutManager.focusedChild)
val position: Int
@ -411,8 +411,8 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
}
override fun handleKeyboardShortcutRepeat(handler: KeyboardShortcutsHandler,
keyCode: Int, repeatCount: Int,
event: KeyEvent, metaState: Int): Boolean {
keyCode: Int, repeatCount: Int,
event: KeyEvent, metaState: Int): Boolean {
return navigationHelper.handleKeyboardShortcutRepeat(handler, keyCode,
repeatCount, event, metaState)
}
@ -426,7 +426,7 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
override fun onLoadFinished(loader: Loader<SingleResponse<ParcelableStatus>>,
data: SingleResponse<ParcelableStatus>) {
data: SingleResponse<ParcelableStatus>) {
val activity = activity ?: return
val status = data.data
if (status != null) {
@ -811,9 +811,9 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
@UiThread
fun displayStatus(account: AccountDetails?,
status: ParcelableStatus?,
statusActivity: StatusActivity?,
translation: TranslationResult?) {
status: ParcelableStatus?,
statusActivity: StatusActivity?,
translation: TranslationResult?) {
if (account == null || status == null) return
val fragment = adapter.fragment
val context = adapter.context
@ -1445,7 +1445,7 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
) : StatusLinkClickHandler(context, manager, preferences) {
override fun onLinkClick(link: String, orig: String?, accountKey: UserKey?,
extraId: Long, type: Int, sensitive: Boolean, start: Int, end: Int): Boolean {
extraId: Long, type: Int, sensitive: Boolean, start: Int, end: Int): Boolean {
val current = getCurrentMedia(link, extraId.toInt())
if (current != null && !current.open_browser) {
expandOrOpenMedia(current)

View File

@ -1,6 +1,7 @@
package org.mariotaku.twidere.model.util
import android.support.annotation.FloatRange
import org.mariotaku.microblog.library.twitter.model.DMResponse
import org.mariotaku.microblog.library.twitter.model.DirectMessage
import org.mariotaku.twidere.model.ParcelableMessage
import org.mariotaku.twidere.model.UserKey
@ -12,11 +13,21 @@ import org.mariotaku.twidere.util.InternalTwitterContentUtils
* Created by mariotaku on 2017/2/9.
*/
object ParcelableMessageUtils {
fun incomingMessage(
accountKey: UserKey,
message: DirectMessage,
@FloatRange(from = 0.0, to = 1.0) sortIdAdj: Double = 0.0
): ParcelableMessage {
fun fromEntry(accountKey: UserKey, entry: DMResponse.Entry): ParcelableMessage? {
when {
entry.message != null -> {
return ParcelableMessage().apply { applyMessage(accountKey, entry.message) }
}
entry.conversationCreate != null -> {
return ParcelableMessage().apply { applyConversationCreate(accountKey, entry.conversationCreate) }
}
}
return null
}
fun incomingMessage(accountKey: UserKey, message: DirectMessage,
@FloatRange(from = 0.0, to = 1.0) sortIdAdj: Double = 0.0): ParcelableMessage {
val result = message(accountKey, message, sortIdAdj)
result.is_outgoing = false
result.conversation_id = incomingConversationId(message.senderId, message.recipientId)
@ -42,6 +53,35 @@ object ParcelableMessageUtils {
return "$senderId-$recipientId"
}
private fun ParcelableMessage.applyMessage(accountKey: UserKey, message: DMResponse.Entry.Message) {
this.commonEntry(accountKey, message)
val data = message.messageData
this.sender_key = UserKey(data.senderId.toString(), accountKey.host)
this.recipient_key = UserKey(data.recipientId.toString(), accountKey.host)
val (text, spans) = InternalTwitterContentUtils.formatDirectMessageText(data)
this.text_unescaped = text
this.spans = spans
this.is_outgoing = this.sender_key == accountKey
}
private fun ParcelableMessage.applyConversationCreate(accountKey: UserKey, message: DMResponse.Entry.Message) {
this.commonEntry(accountKey, message)
this.message_type = ParcelableMessage.MessageType.CONVERSATION_CREATE
this.is_outgoing = false
}
private fun ParcelableMessage.commonEntry(accountKey: UserKey, message: DMResponse.Entry.Message) {
this.message_type = ParcelableMessage.MessageType.TEXT
this.account_key = accountKey
this.id = message.id.toString()
this.conversation_id = message.conversationId
this.message_timestamp = message.time
this.local_timestamp = this.message_timestamp
this.sort_id = this.message_timestamp
}
private fun message(
accountKey: UserKey,
message: DirectMessage,

View File

@ -72,6 +72,32 @@ class GetMessagesTask(
return getDefaultMessages(microBlog, details, param, index)
}
private fun getTwitterOfficialMessages(microBlog: MicroBlog, details: AccountDetails, param: RefreshMessagesTaskParam, index: Int): GetMessagesData {
val accountKey = details.key
val cursor = param.cursors?.get(index)
val page = cursor?.substringAfter("page:").toInt(-1)
val inbox = microBlog.getUserInbox(Paging().apply {
count(60)
if (page >= 0) {
page(page)
}
}).userInbox
val conversations = hashMapOf<String, ParcelableMessageConversation>()
val conversationIds = inbox.conversations.keys
conversations.addLocalConversations(accountKey, conversationIds)
val messages = inbox.entries.mapNotNull { ParcelableMessageUtils.fromEntry(accountKey, it) }
val messagesMap = messages.groupBy(ParcelableMessage::conversation_id)
for ((k, v) in inbox.conversations) {
val message = messagesMap[k]?.maxBy(ParcelableMessage::message_timestamp) ?: continue
val participants = inbox.users.filterKeys { userId ->
v.participants.any { it.userId.toString() == userId }
}.values
conversations.addConversation(k, details, message, participants)
}
return GetMessagesData(conversations.values, messages)
}
private fun getFanfouMessages(microBlog: MicroBlog, details: AccountDetails, param: RefreshMessagesTaskParam, index: Int): GetMessagesData {
val conversationId = param.conversationId
if (conversationId == null) {
@ -81,10 +107,6 @@ class GetMessagesTask(
}
}
private fun getTwitterOfficialMessages(microBlog: MicroBlog, details: AccountDetails, param: RefreshMessagesTaskParam, index: Int): GetMessagesData {
return getDefaultMessages(microBlog, details, param, index)
}
private fun getDefaultMessages(microBlog: MicroBlog, details: AccountDetails, param: RefreshMessagesTaskParam, index: Int): GetMessagesData {
val accountKey = details.key
@ -132,12 +154,12 @@ class GetMessagesTask(
received.forEachIndexed { i, dm ->
val message = ParcelableMessageUtils.incomingMessage(accountKey, dm, 1.0 - (i.toDouble() / received.size))
insertMessages.add(message)
conversations.addConversation(details, message, dm.sender, dm.recipient)
conversations.addConversation(message.conversation_id, details, message, setOf(dm.sender, dm.recipient))
}
sent.forEachIndexed { i, dm ->
val message = ParcelableMessageUtils.outgoingMessage(accountKey, dm, 1.0 - (i.toDouble() / sent.size))
insertMessages.add(message)
conversations.addConversation(details, message, dm.sender, dm.recipient)
conversations.addConversation(message.conversation_id, details, message, setOf(dm.sender, dm.recipient))
}
return GetMessagesData(conversations.values, insertMessages)
}
@ -165,7 +187,8 @@ class GetMessagesTask(
} else {
ParcelableMessageUtils.incomingMessage(accountKey, dm, 1.0 - (i.toDouble() / result.size))
}
val mc = conversations.addConversation(details, message, dm.sender, dm.recipient)
val mc = conversations.addConversation(message.conversation_id, details, message,
setOf(dm.sender, dm.recipient))
mc.request_cursor = "page:$page"
}
return GetMessagesData(conversations.values, emptyList())
@ -231,16 +254,21 @@ class GetMessagesTask(
}
private fun MutableMap<String, ParcelableMessageConversation>.addConversation(
conversationId: String,
details: AccountDetails,
message: ParcelableMessage,
vararg users: User
users: Collection<User>
): ParcelableMessageConversation {
val conversation = this[message.conversation_id] ?: run {
val conversation = this[conversationId] ?: run {
val obj = ParcelableMessageConversation()
obj.id = message.conversation_id
obj.conversation_type = ConversationType.ONE_TO_ONE
obj.id = conversationId
if (users.size == 2) {
obj.conversation_type = ConversationType.ONE_TO_ONE
} else {
obj.conversation_type = ConversationType.GROUP
}
obj.setFrom(message, details)
this[message.conversation_id] = obj
this[conversationId] = obj
return@run obj
}
if (message.timestamp > conversation.timestamp) {

View File

@ -0,0 +1,48 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.view.holder.message
import android.view.View
import kotlinx.android.synthetic.main.list_item_message_conversation_notice.view.*
import org.mariotaku.twidere.R
import org.mariotaku.twidere.adapter.MessagesConversationAdapter
import org.mariotaku.twidere.model.ParcelableMessage
import org.mariotaku.twidere.view.FixedTextView
class ConversationCreateMessageViewHolder(itemView: View, adapter: MessagesConversationAdapter) : AbsMessageViewHolder(itemView, adapter) {
override val messageContent: View = itemView
override val date: FixedTextView by lazy { itemView.date }
private val text by lazy { itemView.text }
override fun display(message: ParcelableMessage, showDate: Boolean) {
super.display(message, showDate)
text.setText(R.string.message_conversation_created)
}
override fun setMessageContentGravity(view: View, outgoing: Boolean) {
// No-op
}
companion object {
const val layoutResource = R.layout.list_item_message_conversation_notice
}
}

View File

@ -71,12 +71,12 @@ class MessageEntryViewHolder(itemView: View, val adapter: MessagesEntriesAdapter
if (user != null) {
adapter.mediaLoader.displayProfileImage(profileImage, user)
} else {
adapter.mediaLoader.cancelDisplayTask(profileImage)
adapter.mediaLoader.displayProfileImage(profileImage, null)
// TODO display default profile image
}
} else {
adapter.mediaLoader.cancelDisplayTask(profileImage)
// TODO display default profile image
profileImage.setImageResource(R.drawable.ic_profile_image_default_group)
}
}

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Twidere - Twitter client for Android
~
~ Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical">
<org.mariotaku.twidere.view.FixedTextView
android:id="@+id/date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="@dimen/element_spacing_normal"
android:textAppearance="?android:textAppearanceSmall"
android:textColor="?android:textColorTertiary"
tools:text="Yesterday"/>
<org.mariotaku.twidere.view.FixedTextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Notice"/>
</LinearLayout>

View File

@ -665,6 +665,7 @@
<string name="message_api_url_format_help">[DOMAIN]: Twitter API domain.\nExample: https://[DOMAIN].twitter.com/ will be replaced to https://api.twitter.com/.</string>
<string name="message_auto_refresh_confirm">Enable auto refresh to get new tweets automatically?</string>
<string name="message_blocked_user">Blocked <xliff:g id="user">%s</xliff:g>.</string>
<string name="message_conversation_created">Conversation created.</string>
<string name="message_direct_message_deleted">Direct message deleted.</string>
<string name="message_direct_message_sent">Direct message sent.</string>
<string name="message_error_invalid_account">Some account data are corrupted, Twidere will remove those accounts to prevent crash.</string>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="48px" height="48px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="background: #1CDCF8;">
<!-- Generator: Sketch 42 (36781) - http://www.bohemiancoding.com/sketch -->
<title>ic_profile_image_default_group-mdpi</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Miscellaneous" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="ic_profile_image_default_group-mdpi">
<polygon id="Shape" fill="#1CDCF8" points="0 0 48 0 48 48 0 48"></polygon>
<path d="M29.3333333,22.6666667 C31.5466667,22.6666667 33.32,20.88 33.32,18.6666667 C33.32,16.4533333 31.5466667,14.6666667 29.3333333,14.6666667 C27.12,14.6666667 25.3333333,16.4533333 25.3333333,18.6666667 C25.3333333,20.88 27.12,22.6666667 29.3333333,22.6666667 Z M18.6666667,22.6666667 C20.88,22.6666667 22.6533333,20.88 22.6533333,18.6666667 C22.6533333,16.4533333 20.88,14.6666667 18.6666667,14.6666667 C16.4533333,14.6666667 14.6666667,16.4533333 14.6666667,18.6666667 C14.6666667,20.88 16.4533333,22.6666667 18.6666667,22.6666667 Z M18.6666667,25.3333333 C15.56,25.3333333 9.33333333,26.8933333 9.33333333,30 L9.33333333,33.3333333 L28,33.3333333 L28,30 C28,26.8933333 21.7733333,25.3333333 18.6666667,25.3333333 Z M29.3333333,25.3333333 C28.9466667,25.3333333 28.5066667,25.36 28.04,25.4 C29.5866667,26.52 30.6666667,28.0266667 30.6666667,30 L30.6666667,33.3333333 L38.6666667,33.3333333 L38.6666667,30 C38.6666667,26.8933333 32.44,25.3333333 29.3333333,25.3333333 Z" id="Shape" fill="#FFFFFF" fill-rule="nonzero"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB