Commit 9ad4a67c authored by Tiago Cunha's avatar Tiago Cunha

WIP: displays and completes suggestion

parent 608df437
......@@ -25,6 +25,8 @@ import android.view.ViewGroup;
import com.jakewharton.rxbinding2.support.v4.widget.RxDrawerLayout;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import java.lang.reflect.Field;
import java.util.ArrayList;
......@@ -53,6 +55,7 @@ import chat.rocket.android.layouthelper.extra_action.upload.AudioUploadActionIte
import chat.rocket.android.layouthelper.extra_action.upload.ImageUploadActionItem;
import chat.rocket.android.layouthelper.extra_action.upload.VideoUploadActionItem;
import chat.rocket.android.log.RCLog;
import chat.rocket.android.renderer.RocketChatUserStatusProvider;
import chat.rocket.android.widget.message.autocomplete.AutocompleteManager;
import chat.rocket.android.widget.message.autocomplete.channel.ChannelSource;
import chat.rocket.android.widget.message.autocomplete.user.UserSource;
......@@ -62,6 +65,7 @@ import chat.rocket.core.interactors.SessionInteractor;
import chat.rocket.core.interactors.UserInteractor;
import chat.rocket.core.models.Message;
import chat.rocket.core.models.Room;
import chat.rocket.core.repositories.UserRepository;
import chat.rocket.persistence.realm.repositories.RealmMessageRepository;
import chat.rocket.persistence.realm.repositories.RealmRoomRepository;
import chat.rocket.persistence.realm.repositories.RealmServerInfoRepository;
......@@ -100,6 +104,8 @@ public class RoomFragment extends AbstractChatRoomFragment
private List<AbstractExtraActionItem> extraActionItems;
private CompositeDisposable compositeDisposable = new CompositeDisposable();
protected RoomContract.Presenter presenter;
public RoomFragment() {
......@@ -249,6 +255,8 @@ public class RoomFragment extends AbstractChatRoomFragment
}
}
compositeDisposable.clear();
if (autocompleteManager != null) {
autocompleteManager.dispose();
autocompleteManager = null;
......@@ -319,14 +327,36 @@ public class RoomFragment extends AbstractChatRoomFragment
)
);
autocompleteManager.registerSource(
new UserSource(
new UserInteractor(new RealmUserRepository(hostname)),
AndroidSchedulers.from(BackgroundLooper.get()),
AndroidSchedulers.mainThread()
)
final UserRepository userRepository = new RealmUserRepository(hostname);
final AbsoluteUrlHelper absoluteUrlHelper = new AbsoluteUrlHelper(
hostname,
new RealmServerInfoRepository(),
userRepository,
new SessionInteractor(new RealmSessionRepository(hostname))
);
Disposable disposable = absoluteUrlHelper.getRocketChatAbsoluteUrl()
.subscribe(
optional -> {
if (optional.isPresent()) {
autocompleteManager.registerSource(
new UserSource(
new UserInteractor(userRepository),
optional.get(),
RocketChatUserStatusProvider.getInstance(),
AndroidSchedulers.from(BackgroundLooper.get()),
AndroidSchedulers.mainThread()
)
);
}
},
throwable -> {
}
);
compositeDisposable.add(disposable);
autocompleteManager.bindTo(
messageFormLayout.getEditText(),
messageFormLayout
......
......@@ -38,8 +38,7 @@ public class AbsoluteUrlHelper {
.filter(Optional::isPresent)
.map(Optional::get),
(info, user, session) -> Optional.of(new RocketChatAbsoluteUrl(
info, user,
session
info, user, session
))
)
.first(Optional.absent());
......
package chat.rocket.android.renderer;
import android.support.annotation.DrawableRes;
import chat.rocket.android.R;
import chat.rocket.android.widget.helper.UserStatusProvider;
import chat.rocket.core.models.User;
public class RocketChatUserStatusProvider implements UserStatusProvider {
private static RocketChatUserStatusProvider INSTANCE;
private RocketChatUserStatusProvider() {
}
public static RocketChatUserStatusProvider getInstance() {
if (INSTANCE == null) {
INSTANCE = new RocketChatUserStatusProvider();
}
return INSTANCE;
}
@Override
@DrawableRes
public int getStatusResId(String status) {
if (User.STATUS_ONLINE.equals(status)) {
return R.drawable.userstatus_online;
} else if (User.STATUS_AWAY.equals(status)) {
return R.drawable.userstatus_away;
} else if (User.STATUS_BUSY.equals(status)) {
return R.drawable.userstatus_busy;
} else if (User.STATUS_OFFLINE.equals(status)) {
return R.drawable.userstatus_offline;
}
// unknown status is rendered as "offline" status.
return R.drawable.userstatus_offline;
}
}
......@@ -4,7 +4,6 @@ import android.content.Context;
import android.widget.ImageView;
import android.widget.TextView;
import chat.rocket.android.R;
import chat.rocket.android.helper.Avatar;
import chat.rocket.android.helper.TextUtils;
import chat.rocket.android.widget.AbsoluteUrl;
......@@ -56,18 +55,7 @@ public class UserRenderer extends AbstractRenderer<User> {
}
String status = object.getStatus();
if (User.STATUS_ONLINE.equals(status)) {
imageView.setImageResource(R.drawable.userstatus_online);
} else if (User.STATUS_AWAY.equals(status)) {
imageView.setImageResource(R.drawable.userstatus_away);
} else if (User.STATUS_BUSY.equals(status)) {
imageView.setImageResource(R.drawable.userstatus_busy);
} else if (User.STATUS_OFFLINE.equals(status)) {
imageView.setImageResource(R.drawable.userstatus_offline);
} else {
// unknown status is rendered as "offline" status.
imageView.setImageResource(R.drawable.userstatus_offline);
}
imageView.setImageResource(RocketChatUserStatusProvider.getInstance().getStatusResId(status));
return this;
}
......
package chat.rocket.android.widget.helper;
import android.support.annotation.StringRes;
import java.util.HashMap;
import chat.rocket.android.widget.R;
public class IconProvider {
private static HashMap<String, Integer> ICON_TABLE = new HashMap<String, Integer>() {
{
put("c", R.string.fa_hashtag);
put("p", R.string.fa_lock);
put("d", R.string.fa_at);
}
};
@StringRes
public static int getIcon(String type) {
if (ICON_TABLE.containsKey(type)) {
return ICON_TABLE.get(type);
}
return ICON_TABLE.get("c");
}
}
package chat.rocket.android.widget.helper;
import android.support.annotation.DrawableRes;
public interface UserStatusProvider {
@DrawableRes
int getStatusResId(String status);
}
package chat.rocket.android.widget.message.autocomplete;
import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
......@@ -8,6 +9,9 @@ import java.util.List;
public abstract class AutocompleteAdapter<I extends AutocompleteItem, H extends AutocompleteViewHolder<I>>
extends RecyclerView.Adapter<H> {
private static final int TYPE_EMPTY = 0;
private static final int TYPE_ITEM = 1;
private List<I> autocompleteItems = new ArrayList<>();
protected AutocompleteViewHolder.OnClickListener<I> onClickListener;
......@@ -17,16 +21,43 @@ public abstract class AutocompleteAdapter<I extends AutocompleteItem, H extends
notifyDataSetChanged();
}
public H onCreateViewHolder(ViewGroup parent, int viewType) {
H holder = getViewHolder(parent);
if (viewType == TYPE_EMPTY) {
holder.showAsEmpty();
}
return holder;
}
@Override
public void onBindViewHolder(H holder, int position) {
if (getItemViewType(position) == TYPE_EMPTY) {
return;
}
holder.bind(autocompleteItems.get(position));
}
@Override
public int getItemCount() {
return autocompleteItems.size();
int count = autocompleteItems.size();
if (count == 0) {
return 1;
}
return count;
}
@Override
public int getItemViewType(int position) {
if (autocompleteItems.size() == 0) {
return TYPE_EMPTY;
}
return TYPE_ITEM;
}
public abstract H getViewHolder(ViewGroup parent);
public void setOnClickListener(AutocompleteViewHolder.OnClickListener<I> onClickListener) {
this.onClickListener = onClickListener;
}
......
package chat.rocket.android.widget.message.autocomplete;
import android.support.annotation.NonNull;
public interface AutocompleteItem {
@NonNull
String getSuggestion();
}
......@@ -2,9 +2,11 @@ package chat.rocket.android.widget.message.autocomplete;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.EditText;
import android.widget.RelativeLayout;
......@@ -39,6 +41,9 @@ public class AutocompleteManager {
private final View contentHolder;
private final RecyclerView recyclerView;
private float contentHolderInitialY;
private final float yOffset;
private final AutocompleteSource.OnAutocompleteSelected onAutocompleteSelected =
new AutocompleteSource.OnAutocompleteSelected() {
@Override
......@@ -57,6 +62,12 @@ public class AutocompleteManager {
recyclerView.setLayoutManager(new LinearLayoutManager(parent.getContext()));
parent.addView(contentHolder);
yOffset = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
32,
contentHolder.getContext().getResources().getDisplayMetrics()
);
}
public void registerSource(AutocompleteSource autocompleteSource) {
......@@ -70,9 +81,17 @@ public class AutocompleteManager {
RelativeLayout.LayoutParams layoutParams =
(RelativeLayout.LayoutParams) contentHolder.getLayoutParams();
layoutParams.addRule(RelativeLayout.ABOVE, anchor.getId());
}
contentHolder.setVisibility(View.GONE);
contentHolder.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
contentHolderInitialY = contentHolder.getY();
animateHide();
contentHolder.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
}
afterTextChangeDisposable = RxTextView.afterTextChangeEvents(editText)
.debounce(300, TimeUnit.MILLISECONDS)
......@@ -151,7 +170,7 @@ public class AutocompleteManager {
this.text = text;
contentHolder.setVisibility(View.VISIBLE);
animateShow();
sourceDisposable.clear();
......@@ -159,7 +178,7 @@ public class AutocompleteManager {
}
private void cleanState() {
contentHolder.setVisibility(View.GONE);
animateHide();
sourceDisposable.clear();
......@@ -222,4 +241,32 @@ public class AutocompleteManager {
editText.setText(stringBuilder.toString());
editText.setSelection(selectionPos);
}
private void animateHide() {
contentHolder.animate().cancel();
contentHolder.animate()
.alpha(0)
.translationY(contentHolderInitialY + yOffset)
.setDuration(150)
.withEndAction(new Runnable() {
@Override
public void run() {
contentHolder.setVisibility(View.GONE);
}
});
}
private void animateShow() {
contentHolder.animate().cancel();
contentHolder.animate()
.alpha(1)
.translationY(contentHolderInitialY)
.setDuration(150)
.withStartAction(new Runnable() {
@Override
public void run() {
contentHolder.setVisibility(View.VISIBLE);
}
});
}
}
......@@ -11,6 +11,8 @@ public abstract class AutocompleteViewHolder<I extends AutocompleteItem>
public abstract void bind(I autocompleteItem);
public abstract void showAsEmpty();
public interface OnClickListener<I extends AutocompleteItem> {
void onClick(I autocompleteItem);
}
......
......@@ -10,7 +10,7 @@ import chat.rocket.android.widget.message.autocomplete.AutocompleteAdapter;
public class ChannelAdapter extends AutocompleteAdapter<ChannelItem, ChannelViewHolder> {
@Override
public ChannelViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
public ChannelViewHolder getViewHolder(ViewGroup parent) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.autocomplete_channel_view, parent, false);
......
package chat.rocket.android.widget.message.autocomplete.channel;
import android.support.annotation.NonNull;
import android.support.annotation.StringRes;
import chat.rocket.android.widget.helper.IconProvider;
import chat.rocket.android.widget.message.autocomplete.AutocompleteItem;
import chat.rocket.core.models.Room;
......@@ -13,7 +15,14 @@ public class ChannelItem implements AutocompleteItem {
this.room = room;
}
public String getTitle() {
@NonNull
@Override
public String getSuggestion() {
return room.getName();
}
@StringRes
public int getIcon() {
return IconProvider.getIcon(room.getType());
}
}
......@@ -53,6 +53,7 @@ public class ChannelSource extends AutocompleteSource<ChannelAdapter, ChannelIte
return roomInteractor.getRoomsWithNameLike(s);
}
})
.distinctUntilChanged()
.map(new Function<List<Room>, List<ChannelItem>>() {
@Override
public List<ChannelItem> apply(@io.reactivex.annotations.NonNull List<Room> rooms)
......@@ -93,7 +94,7 @@ public class ChannelSource extends AutocompleteSource<ChannelAdapter, ChannelIte
@Override
protected String getAutocompleteSuggestion(ChannelItem autocompleteItem) {
return getTrigger() + autocompleteItem.getTitle();
return getTrigger() + autocompleteItem.getSuggestion();
}
private List<ChannelItem> toChannelItemList(List<Room> rooms) {
......
......@@ -10,12 +10,14 @@ import chat.rocket.android.widget.message.autocomplete.AutocompleteViewHolder;
public class ChannelViewHolder extends AutocompleteViewHolder<ChannelItem> {
private final TextView titleTextView;
private final TextView iconTextView;
public ChannelViewHolder(View itemView,
final AutocompleteViewHolder.OnClickListener<ChannelItem> onClickListener) {
super(itemView);
titleTextView = (TextView) itemView.findViewById(R.id.title);
iconTextView = (TextView) itemView.findViewById(R.id.icon);
itemView.setOnClickListener(new View.OnClickListener() {
@Override
......@@ -32,7 +34,17 @@ public class ChannelViewHolder extends AutocompleteViewHolder<ChannelItem> {
itemView.setTag(channelItem);
if (titleTextView != null) {
titleTextView.setText(channelItem.getTitle());
titleTextView.setText(channelItem.getSuggestion());
}
if (iconTextView != null) {
iconTextView.setText(channelItem.getIcon());
}
}
@Override
public void showAsEmpty() {
iconTextView.setVisibility(View.GONE);
titleTextView.setText(R.string.no_channel_found);
}
}
......@@ -10,9 +10,9 @@ import chat.rocket.android.widget.message.autocomplete.AutocompleteAdapter;
public class UserAdapter extends AutocompleteAdapter<UserItem, UserViewHolder> {
@Override
public UserViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
public UserViewHolder getViewHolder(ViewGroup parent) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.autocomplete_channel_view, parent, false);
.inflate(R.layout.autocomplete_user_view, parent, false);
return new UserViewHolder(view, onClickListener);
}
......
package chat.rocket.android.widget.message.autocomplete.user;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import chat.rocket.android.widget.AbsoluteUrl;
import chat.rocket.android.widget.helper.UserStatusProvider;
import chat.rocket.android.widget.message.autocomplete.AutocompleteItem;
import chat.rocket.core.models.User;
public class UserItem implements AutocompleteItem {
private final User user;
private final AbsoluteUrl absoluteUrl;
private final UserStatusProvider userStatusProvider;
public UserItem(@NonNull User user) {
public UserItem(@NonNull User user, AbsoluteUrl absoluteUrl,
UserStatusProvider userStatusProvider) {
this.user = user;
this.absoluteUrl = absoluteUrl;
this.userStatusProvider = userStatusProvider;
}
public String getTitle() {
@NonNull
@Override
public String getSuggestion() {
//noinspection ConstantConditions
return user.getUsername();
}
public AbsoluteUrl getAbsoluteUrl() {
return absoluteUrl;
}
@DrawableRes
public int getStatusResId() {
return userStatusProvider.getStatusResId(user.getStatus());
}
}
......@@ -11,6 +11,8 @@ import org.reactivestreams.Publisher;
import java.util.ArrayList;
import java.util.List;
import chat.rocket.android.widget.AbsoluteUrl;
import chat.rocket.android.widget.helper.UserStatusProvider;
import chat.rocket.android.widget.message.autocomplete.AutocompleteSource;
import chat.rocket.core.interactors.UserInteractor;
import chat.rocket.core.models.User;
......@@ -18,11 +20,17 @@ import chat.rocket.core.models.User;
public class UserSource extends AutocompleteSource<UserAdapter, UserItem> {
private final UserInteractor userInteractor;
private final AbsoluteUrl absoluteUrl;
private final UserStatusProvider userStatusProvider;
private final Scheduler bgScheduler;
private final Scheduler fgScheduler;
public UserSource(UserInteractor userInteractor, Scheduler bgScheduler, Scheduler fgScheduler) {
public UserSource(UserInteractor userInteractor, AbsoluteUrl absoluteUrl,
UserStatusProvider userStatusProvider,
Scheduler bgScheduler, Scheduler fgScheduler) {
this.userInteractor = userInteractor;
this.absoluteUrl = absoluteUrl;
this.userStatusProvider = userStatusProvider;
this.bgScheduler = bgScheduler;
this.fgScheduler = fgScheduler;
}
......@@ -50,6 +58,7 @@ public class UserSource extends AutocompleteSource<UserAdapter, UserItem> {
return userInteractor.getUserAutocompleteSuggestions(s);
}
})
.distinctUntilChanged()
.map(new Function<List<User>, List<UserItem>>() {
@Override
public List<UserItem> apply(@io.reactivex.annotations.NonNull List<User> users)
......@@ -90,7 +99,7 @@ public class UserSource extends AutocompleteSource<UserAdapter, UserItem> {
@Override
protected String getAutocompleteSuggestion(UserItem autocompleteItem) {
return getTrigger() + autocompleteItem.getTitle();
return getTrigger() + autocompleteItem.getSuggestion();
}
private List<UserItem> toUserItemList(List<User> users) {
......@@ -98,7 +107,7 @@ public class UserSource extends AutocompleteSource<UserAdapter, UserItem> {
List<UserItem> userItems = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
userItems.add(new UserItem(users.get(i)));
userItems.add(new UserItem(users.get(i), absoluteUrl, userStatusProvider));
}
return userItems;
......
package chat.rocket.android.widget.message.autocomplete.user;
import android.content.Context;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.amulyakhare.textdrawable.TextDrawable;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import chat.rocket.android.widget.AbsoluteUrl;
import chat.rocket.android.widget.R;
import chat.rocket.android.widget.RocketChatAvatar;
import chat.rocket.android.widget.message.autocomplete.AutocompleteViewHolder;
public class UserViewHolder extends AutocompleteViewHolder<UserItem> {
private static final int[] COLORS = new int[]{
0xFFF44336, 0xFFE91E63, 0xFF9C27B0, 0xFF673AB7, 0xFF3F51B5, 0xFF2196F3,
0xFF03A9F4, 0xFF00BCD4, 0xFF009688, 0xFF4CAF50, 0xFF8BC34A, 0xFFCDDC39,
0xFFFFC107, 0xFFFF9800, 0xFFFF5722, 0xFF795548, 0xFF9E9E9E, 0xFF607D8B
};
private final TextView titleTextView;
private final RocketChatAvatar avatar;
private final ImageView status;
public UserViewHolder(View itemView,
final AutocompleteViewHolder.OnClickListener<UserItem> onClickListener) {
super(itemView);
titleTextView = (TextView) itemView.findViewById(R.id.title);
avatar = (RocketChatAvatar) itemView.findViewById(R.id.avatar);
status = (ImageView) itemView.findViewById(R.id.status);
itemView.setOnClickListener(new View.OnClickListener() {
@Override
......@@ -30,8 +51,78 @@ public class UserViewHolder extends AutocompleteViewHolder<UserItem> {
public void bind(UserItem userItem) {
itemView.setTag(userItem);
final String suggestion = userItem.getSuggestion();
if (titleTextView != null) {
titleTextView.setText(userItem.getTitle());
titleTextView.setText(suggestion);
}
if (avatar != null) {
avatar.loadImage(
getImageUrl(suggestion, userItem.getAbsoluteUrl()),
getTextDrawable(itemView.getContext(), suggestion)
);
}
if (status != null) {
status.setImageResource(userItem.getStatusResId());
}
}
@Override
public void showAsEmpty() {
status.setVisibility(View.GONE);
avatar.setVisibility(View.GONE);
titleTextView.setText(R.string.no_user_found);
}
private String getImageUrl(String username, AbsoluteUrl absoluteUrl) {
//from Rocket.Chat:packages/rocketchat-ui/lib/avatar.coffee
//REMARK! this is often SVG image! (see: Rocket.Chat:server/startup/avatar.coffee)
try {
final String avatarUrl = "/avatar/" + URLEncoder.encode(username, "UTF-8") + ".jpg";
if (absoluteUrl == null) {
return avatarUrl;
}
return absoluteUrl.from(avatarUrl);
} catch (UnsupportedEncodingException exception) {
return null;
}
}
private Drawable getTextDrawable(Context context, String username) {
if (username == null) {
return null;
}
int round = (int) (4 * context.getResources().getDisplayMetrics().density);
return TextDrawable.builder()
.beginConfig()
.useFont(Typeface.SANS_SERIF)
.endConfig()
.buildRoundRect(getInitialsForUser(username), getColorForUser(username), round);
}
private int getColorForUser(String username) {
return COLORS[username.length() % COLORS.length];
}
private String getInitialsForUser(String username) {
String name = username
.replaceAll("[^A-Za-z0-9]", ".")
.replaceAll("\\.+", ".")
.replaceAll("(^\\.)|(\\.$)", "");
String[] initials = name.split("\\.");
if (initials.length >= 2) {
return (firstChar(initials[0]) + firstChar(initials[initials.length - 1])).toUpperCase();
} else {
String name2 = name.replaceAll("[^A-Za-z0-9]", "");
return (name2.length() < 2) ? name2 : name2.substring(0, 2).toUpperCase();
}
}
private static String firstChar(String str) {
return TextUtils.isEmpty(str) ? "" : str.substring(0, 1);
}
}
......@@ -5,11 +5,21 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:gravity="center_vertical"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<io.github.yusukeiwaki.android.widget.FontAwesomeTextView
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
tools:text="#" />
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
......
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:gravity="center_vertical"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<ImageView
android:id="@+id/status"
android:layout_width="8dp"
android:layout_height="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp" />
<chat.rocket.android.widget.RocketChatAvatar
android:id="@+id/avatar"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp" />
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="YOLO" />
</LinearLayout>
\ No newline at end of file
......@@ -2,4 +2,6 @@
<resources>
<string name="message_composer_message_hint">Message</string>
<string name="click_to_load">Click to load</string>
<string name="no_channel_found">No channel found</string>
<string name="no_user_found">No user found</string>
</resources>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment