/*
* Copyright (C) 2014 Eric Butler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tapchatapp.android.app.ui;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Typeface;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan;
import android.util.Pair;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.tapchatapp.android.R;
import com.tapchatapp.android.client.message.AwayMessage;
import com.tapchatapp.android.client.message.BannedMessage;
import com.tapchatapp.android.client.message.BufferEventMessage;
import com.tapchatapp.android.client.message.BufferMeMsgMessage;
import com.tapchatapp.android.client.message.BufferMsgMessage;
import com.tapchatapp.android.client.message.ChannelModeIsMessage;
import com.tapchatapp.android.client.message.ChannelModeMessage;
import com.tapchatapp.android.client.message.ChannelTimestampMessage;
import com.tapchatapp.android.client.message.ChannelTopicMessage;
import com.tapchatapp.android.client.message.ChannelUrlMessage;
import com.tapchatapp.android.client.message.ConnectedMessage;
import com.tapchatapp.android.client.message.ConnectingFailedMessage;
import com.tapchatapp.android.client.message.ConnectingMessage;
import com.tapchatapp.android.client.message.ConnectingRetryMessage;
import com.tapchatapp.android.client.message.JoinedChannelMessage;
import com.tapchatapp.android.client.message.KickedChannelMessage;
import com.tapchatapp.android.client.message.NickchangeMessage;
import com.tapchatapp.android.client.message.NoticeMessage;
import com.tapchatapp.android.client.message.PartedChannelMessage;
import com.tapchatapp.android.client.message.QuitServerMessage;
import com.tapchatapp.android.client.message.SocketClosedMessage;
import com.tapchatapp.android.client.message.UserChannelModeMessage;
import com.tapchatapp.android.client.message.UserModeMessage;
import com.tapchatapp.android.client.message.WaitingToRetryMessage;
import com.tapchatapp.android.client.message.YouJoinedChannelMessage;
import com.tapchatapp.android.client.message.YouNickchangeMessage;
import com.tapchatapp.android.client.message.YouPartedChannelMessage;
import com.tapchatapp.android.client.message.request.QuitMessage;
import com.tapchatapp.android.client.model.BufferEvent;
import com.tapchatapp.android.client.model.BufferEventItem;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Iterables.transform;
import static com.google.common.collect.Lists.newArrayList;
public class BufferEventRenderer {
private static final List<String> PRESENCE_TYPES = ImmutableList.of("joined_channel", "parted_channel", "quit");
private Context mContext;
private boolean mIncludeTimestamp;
private final int mNickColor;
private final SimpleDateFormat mDateFormat = new SimpleDateFormat("h:mm aa");
public BufferEventRenderer(Context context) {
this(context, false);
}
public BufferEventRenderer(Context context, boolean includeTimestamp) {
mContext = context;
mIncludeTimestamp = includeTimestamp;
TypedArray typedArray = context.obtainStyledAttributes(R.styleable.Tapchat);
mNickColor = typedArray.getColor(R.styleable.Tapchat_nickColor, Color.BLACK);
}
public CharSequence renderEvent(BufferEvent event) {
return addTimestamp(renderEventReal(event), event.getFirstItem().getMessage().getDate());
}
public CharSequence renderEventItem(BufferEventItem event) {
return addTimestamp(renderEventItemReal(event), event.getMessage().getDate());
}
private CharSequence renderEventReal(BufferEvent event) {
if (event.getItems().size() > 1) {
Map<String, String> presenceChanges = new HashMap<>();
List<Pair<String, String>> nickChanges = new ArrayList<>();
for (BufferEventItem item : event.getItems()) {
BufferEventMessage message = item.getMessage();
String type = message.type;
if (PRESENCE_TYPES.contains(type)) {
presenceChanges.put(message.nick, type);
} else if (type.equals("nickchange")) {
NickchangeMessage nickchangeMessage = (NickchangeMessage) message;
String oldnick = nickchangeMessage.oldnick;
String newnick = nickchangeMessage.newnick;
// Update any "joined" events
if (presenceChanges.containsKey(oldnick) && presenceChanges.get(oldnick).equals("joined_channel")) {
String presence = presenceChanges.remove(oldnick);
presenceChanges.put(newnick, presence);
}
nickChanges.add(new Pair<>(oldnick, newnick));
}
}
List<String> strings = newArrayList();
List<String> seenNicks = newArrayList();
appendPresenceEvents(strings, presenceChanges, "joined_channel", R.string.joined_format, nickChanges, seenNicks);
appendPresenceEvents(strings, presenceChanges, "parted_channel", R.string.parted_format, nickChanges, seenNicks);
appendPresenceEvents(strings, presenceChanges, "quit", R.string.quit_format, nickChanges, seenNicks);
for (Pair<String, String> nickChange : nickChanges) {
if (!seenNicks.contains(nickChange.first) && !seenNicks.contains(nickChange.second)) {
List<String> chain = getNickChain(nickChanges, nickChange.first);
seenNicks.addAll(chain);
String firstNick = chain.get(0);
String lastNick = chain.get(chain.size() - 1);
strings.add(String.format("%s → %s", firstNick, lastNick));
}
}
return Joiner.on(" • ").join(strings);
} else {
return renderEventItem(event.getFirstItem());
}
}
private CharSequence renderEventItemReal(BufferEventItem event) {
BufferEventMessage message = event.getMessage();
String type = message.type;
switch (type) {
case SocketClosedMessage.TYPE:
return mContext.getString(R.string.event_socket_closed);
case ConnectingMessage.TYPE:
// addStatusLine(String.format("Connecting to %s", message.getString("hostname")));
return mContext.getString(R.string.event_connecting);
case ConnectedMessage.TYPE:
ConnectedMessage connectedMessage = (ConnectedMessage) message;
return mContext.getString(R.string.event_connected, connectedMessage.hostname);
case QuitServerMessage.TYPE:
return mContext.getString(R.string.event_quit_server);
case NoticeMessage.TYPE:
if (!TextUtils.isEmpty(message.from)) {
SpannableString span = new SpannableString(String.format("%s %s", message.from, event.getMessage().getMsgString()));
span.setSpan(new StyleSpan(Typeface.BOLD), 0, message.from.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
span.setSpan(new TypefaceSpan("monospace"), 0, span.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
return span;
} else {
SpannableString span = new SpannableString(event.getMessage().getMsgString());
span.setSpan(new TypefaceSpan("monospace"), 0, span.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
return span;
}
case YouNickchangeMessage.TYPE:
YouNickchangeMessage youNickchangeMessage = (YouNickchangeMessage) message;
return mContext.getString(R.string.event_you_nickchange, youNickchangeMessage.newnick);
case BannedMessage.TYPE:
return mContext.getString(R.string.event_banned);
case ConnectingRetryMessage.TYPE: {
ConnectingRetryMessage retryMessage = (ConnectingRetryMessage) message;
return mContext.getString(R.string.event_connecting_retry, retryMessage.interval);
}
case WaitingToRetryMessage.TYPE: {
WaitingToRetryMessage retryMessage = (WaitingToRetryMessage) message;
return mContext.getString(R.string.event_waiting_to_retry, retryMessage.interval);
}
case ConnectingFailedMessage.TYPE:
return mContext.getString(R.string.event_connecting_failed);
// FIXME:
// } else if (type.equals("joining")) {
// JSONArray channels = event.getJSONArray("channels");
// return mContext.getString(R.string.event_joining, channels);
case UserModeMessage.TYPE:
UserModeMessage modeMessage = (UserModeMessage) message;
return mContext.getString(R.string.event_user_mode, modeMessage.newmode);
case BufferMsgMessage.TYPE: {
SpannableString span = new SpannableString(String.format("%s %s", message.from, event.getMessage().msg));
formatName(span, 0, message.from.length());
return span;
}
case BufferMeMsgMessage.TYPE: {
SpannableString span = new SpannableString(String.format("• %s %s", message.from, event.getMessage().msg));
formatName(span, 0, message.from.length() + 2);
return span;
}
case JoinedChannelMessage.TYPE:
JoinedChannelMessage joinedChannelMessage = (JoinedChannelMessage) message;
return mContext.getString(R.string.event_joined_channel, joinedChannelMessage.nick);
case PartedChannelMessage.TYPE:
PartedChannelMessage partedChannelMessage = (PartedChannelMessage) message;
return mContext.getString(R.string.event_parted_channel, partedChannelMessage.nick);
case QuitMessage.TYPE:
QuitMessage quitMessage = (QuitMessage) message;
return mContext.getString(R.string.event_quit, quitMessage.nick);
case AwayMessage.TYPE:
AwayMessage awayMessage = (AwayMessage) message;
return mContext.getString(R.string.event_away, awayMessage.nick);
case KickedChannelMessage.TYPE:
KickedChannelMessage kickedChannelMessage = (KickedChannelMessage) message;
return mContext.getString(R.string.event_kicked_channel,
kickedChannelMessage.nick,
kickedChannelMessage.chan,
kickedChannelMessage.kicker,
kickedChannelMessage.hostmask,
kickedChannelMessage.msg);
case YouJoinedChannelMessage.TYPE:
return mContext.getString(R.string.event_you_joined_channel);
case YouPartedChannelMessage.TYPE:
return mContext.getString(R.string.event_you_parted_channel);
case ChannelModeIsMessage.TYPE:
ChannelModeIsMessage channelModeIsMessage = (ChannelModeIsMessage) message;
return mContext.getString(R.string.event_channel_mode_is, channelModeIsMessage.newmode);
case ChannelTimestampMessage.TYPE:
ChannelTimestampMessage channelTimestampMessage = (ChannelTimestampMessage) message;
return mContext.getString(R.string.event_channel_timestamp, channelTimestampMessage.timestamp);
case NickchangeMessage.TYPE:
NickchangeMessage nickchangeMessage = (NickchangeMessage) message;
return mContext.getString(R.string.event_nickchange, nickchangeMessage.oldnick, nickchangeMessage.newnick);
case UserChannelModeMessage.TYPE:
UserChannelModeMessage userChannelModeMessage = (UserChannelModeMessage) message;
return mContext.getString(R.string.event_user_channel_mode,
userChannelModeMessage.diff,
userChannelModeMessage.nick,
userChannelModeMessage.from);
case ChannelUrlMessage.TYPE:
// FIXME: This should show up next to the topic instead of in the buffer
ChannelUrlMessage channelUrlMessage = (ChannelUrlMessage) message;
return mContext.getString(R.string.event_channel_url, channelUrlMessage.url);
case ChannelTopicMessage.TYPE:
ChannelTopicMessage channelTopicMessage = (ChannelTopicMessage) message;
String topic = channelTopicMessage.topic;
String nick = channelTopicMessage.author.split("!")[0];
if (!TextUtils.isEmpty(topic)) {
return mContext.getString(R.string.event_channel_topic, nick, topic);
} else {
return mContext.getString(R.string.event_channel_topic_cleared, nick);
}
case ChannelModeMessage.TYPE:
ChannelModeMessage channelModeMessage = (ChannelModeMessage) message;
String mode = channelModeMessage.diff;
if (TextUtils.isEmpty(mode)) {
mode = channelModeMessage.newmode;
}
return mContext.getString(R.string.event_channel_mode, mode, message.from);
default:
if (!TextUtils.isEmpty(message.getMsgString())) {
return message.getMsgString();
}
return "Unknown message type '" + type + "': " + event.toString();
}
}
private CharSequence addTimestamp(CharSequence text, Date date) {
if (mIncludeTimestamp) {
return new SpannableStringBuilder()
.append(mDateFormat.format(date))
.append(" ")
.append(text);
}
return text;
}
private void appendPresenceEvents(List<String> strings, final Map<String, String> presenceChanges,
final String type, int resId, final List<Pair<String, String>> nickChanges, final List<String> seenNicks) {
List<String> nicks = newArrayList(filter(presenceChanges.keySet(), new Predicate<String>() {
@Override public boolean apply(String nick) {
return presenceChanges.get(nick).equals(type);
}
}));
if (nicks.isEmpty()) {
return;
}
nicks = newArrayList(transform(nicks, new Function<String, String>() {
@Override public String apply(String nick) {
List<String> nickChain = getNickChainReverse(nickChanges, nick);
if (nickChain.size() > 1) {
// Last item is current nick
nickChain.remove(nickChain.size() - 1);
// Don't need to show nick change if included in presence event
seenNicks.addAll(nickChain);
return mContext.getString(R.string.event_was_format, nick, Joiner.on(", ").join(nickChain));
} else {
return nick;
}
}
}));
strings.add(mContext.getString(resId, Joiner.on(", ").join(nicks)));
}
private List<String> getNickChain(List<Pair<String, String>> nickChanges, String startNick) {
List<String> chain = newArrayList();
chain.add(startNick);
for (Pair<String, String> nickChange : nickChanges) {
String lastNick = chain.get(chain.size() - 1);
if (nickChange.first.equals(lastNick)) {
chain.add(nickChange.second);
}
}
return chain;
}
private List<String> getNickChainReverse(List<Pair<String, String>> nickChanges, String endNick) {
List<String> chain = newArrayList();
chain.add(endNick);
for (Pair<String, String> nickChange : Lists.reverse(nickChanges)) {
String lastNick = chain.get(chain.size() - 1);
if (nickChange.second.equals(lastNick)) {
chain.add(nickChange.first);
}
}
return Lists.reverse(chain);
}
private void formatName(SpannableString span, int start, int end) {
span.setSpan(new StyleSpan(Typeface.BOLD), start, end, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
span.setSpan(new ForegroundColorSpan(mNickColor), start, end, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
}
}