diff --git a/app/src/main/java/de/kuschku/libquassel/syncables/types/impl/IrcChannel.java b/app/src/main/java/de/kuschku/libquassel/syncables/types/impl/IrcChannel.java index 9c9bad616b958bc07746538aa1bad0cbd09514cc..3149c7c2044f0a1a56068051336718f51285fca0 100644 --- a/app/src/main/java/de/kuschku/libquassel/syncables/types/impl/IrcChannel.java +++ b/app/src/main/java/de/kuschku/libquassel/syncables/types/impl/IrcChannel.java @@ -43,6 +43,7 @@ import de.kuschku.libquassel.syncables.types.abstracts.AIrcChannel; import de.kuschku.libquassel.syncables.types.interfaces.QIrcUser; import de.kuschku.libquassel.syncables.types.interfaces.QNetwork; import de.kuschku.util.irc.ModeUtils; +import de.kuschku.util.observables.lists.ObservableSet; import static de.kuschku.util.AndroidAssert.assertEquals; @@ -50,6 +51,7 @@ public class IrcChannel extends AIrcChannel<IrcChannel> { private final String name; @NonNull private final Map<QIrcUser, Set<Character>> userModes = new HashMap<>(); + private final ObservableSet<QIrcUser> users = new ObservableSet<>(); private String topic; private String password; private boolean encrypted; @@ -285,6 +287,7 @@ public class IrcChannel extends AIrcChannel<IrcChannel> { _addUserMode(ircuser, mode); } else { userModes.put(ircuser, ModeUtils.toModes(mode)); + users.add(ircuser); ircuser._joinChannel(this, true); _update(); } @@ -294,11 +297,13 @@ public class IrcChannel extends AIrcChannel<IrcChannel> { public void _part(@NonNull QIrcUser ircuser) { if (isKnownUser(ircuser)) { userModes.remove(ircuser); + users.remove(ircuser); ircuser._partChannel(this); if (network().isMe(ircuser) || userModes.isEmpty()) { Set<QIrcUser> users = userModes.keySet(); userModes.clear(); + users.clear(); for (QIrcUser user : users) { user._partChannel(this, true); } @@ -318,6 +323,7 @@ public class IrcChannel extends AIrcChannel<IrcChannel> { if (isKnownUser(ircuser)) { userModes.put(ircuser, ModeUtils.toModes(modes)); + users.add(ircuser); _update(); } } @@ -334,6 +340,7 @@ public class IrcChannel extends AIrcChannel<IrcChannel> { if (!userModes.get(ircuser).contains(ModeUtils.toMode(mode))) { userModes.get(ircuser).add(ModeUtils.toMode(mode)); + users.notifyItemChanged(ircuser); _update(); } } @@ -421,9 +428,14 @@ public class IrcChannel extends AIrcChannel<IrcChannel> { this.network = network; + /* TODO: Use just the nick in userModes and users instead – that should make sync things a lot easier */ if (cachedUserModes != null) { for (String username : cachedUserModes.keySet()) { - userModes.put(network().ircUser(username), ModeUtils.toModes(cachedUserModes.get(username))); + QIrcUser ircUser = network().ircUser(username); + if (ircUser != null) { + userModes.put(ircUser, ModeUtils.toModes(cachedUserModes.get(username))); + users.add(ircUser); + } } } if (cachedChanModes != null) { @@ -483,4 +495,8 @@ public class IrcChannel extends AIrcChannel<IrcChannel> { assertEquals(split.length, 2); init(client.networkManager().network(Integer.parseInt(split[0])), client); } + + public ObservableSet<QIrcUser> users() { + return users; + } } diff --git a/app/src/main/java/de/kuschku/libquassel/syncables/types/impl/IrcUser.java b/app/src/main/java/de/kuschku/libquassel/syncables/types/impl/IrcUser.java index c76bb718cc6f1f43761c8b1bd422fc038f1734ce..e33a0e401f0a6a07d37dddf0691f750e987192d1 100644 --- a/app/src/main/java/de/kuschku/libquassel/syncables/types/impl/IrcUser.java +++ b/app/src/main/java/de/kuschku/libquassel/syncables/types/impl/IrcUser.java @@ -458,4 +458,9 @@ public class IrcUser extends AIrcUser<IrcUser> { @Override public void _update(IrcUser from) { } + + @Override + public String toString() { + return "IrcUser{" + hostmask() + '}'; + } } diff --git a/app/src/main/java/de/kuschku/libquassel/syncables/types/impl/Network.java b/app/src/main/java/de/kuschku/libquassel/syncables/types/impl/Network.java index c062cf6e02d7feff4ff09e288eb3ff5397ba8c0a..b42e6bf9d0e4092203ab92be13124e72d3e18b00 100644 --- a/app/src/main/java/de/kuschku/libquassel/syncables/types/impl/Network.java +++ b/app/src/main/java/de/kuschku/libquassel/syncables/types/impl/Network.java @@ -41,6 +41,7 @@ import de.kuschku.libquassel.syncables.types.abstracts.ANetwork; import de.kuschku.libquassel.syncables.types.interfaces.QIrcChannel; import de.kuschku.libquassel.syncables.types.interfaces.QIrcUser; import de.kuschku.libquassel.syncables.types.interfaces.QNetwork; +import de.kuschku.util.CompatibilityUtils; import de.kuschku.util.irc.IrcCaseMapper; import de.kuschku.util.irc.IrcUserUtils; import de.kuschku.util.irc.ModeUtils; @@ -178,6 +179,12 @@ public class Network extends ANetwork<Network> implements Observer { return ""; } + @Override + public int modeToIndex(String mode) { + int index = prefixModes().indexOf(mode); + return index == -1 ? Integer.MAX_VALUE : index; + } + @Override public ChannelModeType channelModeType(char mode) { return channelModeType(ModeUtils.fromMode(mode)); @@ -334,11 +341,12 @@ public class Network extends ANetwork<Network> implements Observer { String prefix = support("PREFIX"); if (prefix.startsWith("(") && prefix.contains(")")) { - prefixes = Arrays.asList(prefix.split("\\)")[1].split("")); - prefixModes = Arrays.asList(prefix.substring(1).split("\\)")[0].split("")); + String[] data = prefix.substring(1).split("\\)"); + prefixes = Arrays.asList(CompatibilityUtils.partStringByChar(data[1])); + prefixModes = Arrays.asList(CompatibilityUtils.partStringByChar(data[0])); } else { - List<String> defaultPrefixes = Arrays.asList("~&@%+".split("")); - List<String> defaultPrefixModes = Arrays.asList("qaohv".split("")); + List<String> defaultPrefixes = Arrays.asList(CompatibilityUtils.partStringByChar("~&@%+")); + List<String> defaultPrefixModes = Arrays.asList(CompatibilityUtils.partStringByChar("qaohv")); if (prefix.isEmpty()) { prefixes = defaultPrefixes; diff --git a/app/src/main/java/de/kuschku/libquassel/syncables/types/interfaces/QIrcChannel.java b/app/src/main/java/de/kuschku/libquassel/syncables/types/interfaces/QIrcChannel.java index a1a406d57ff86814179f78db9e8fbd69d4323b5c..05486e16cd08b387dc5b6e08c375560eba188b15 100644 --- a/app/src/main/java/de/kuschku/libquassel/syncables/types/interfaces/QIrcChannel.java +++ b/app/src/main/java/de/kuschku/libquassel/syncables/types/interfaces/QIrcChannel.java @@ -27,6 +27,7 @@ import java.util.List; import de.kuschku.libquassel.client.Client; import de.kuschku.libquassel.syncables.Synced; +import de.kuschku.util.observables.lists.ObservableSet; public interface QIrcChannel extends QObservable { boolean isKnownUser(QIrcUser ircuser); @@ -148,4 +149,6 @@ public interface QIrcChannel extends QObservable { void init(QNetwork network, Client client); String getObjectName(); + + ObservableSet<QIrcUser> users(); } diff --git a/app/src/main/java/de/kuschku/libquassel/syncables/types/interfaces/QNetwork.java b/app/src/main/java/de/kuschku/libquassel/syncables/types/interfaces/QNetwork.java index 5ea8da726ba7a2f04076d275fb9e3063d8cfdc9f..94093c47a627d465a4f12ba9c7cc4e0d8edca11f 100644 --- a/app/src/main/java/de/kuschku/libquassel/syncables/types/interfaces/QNetwork.java +++ b/app/src/main/java/de/kuschku/libquassel/syncables/types/interfaces/QNetwork.java @@ -55,6 +55,8 @@ public interface QNetwork extends QObservable { String modeToPrefix(final String mode); + int modeToIndex(String mode); + ChannelModeType channelModeType(final char mode); ChannelModeType channelModeType(final String mode); diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/ChatActivity.java b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/ChatActivity.java index 07901428c7400286c8d2bd6254a13dc2f778509c..6c800d565699226a8ecd66a26098efc5e6828019 100644 --- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/ChatActivity.java +++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/ChatActivity.java @@ -32,6 +32,7 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.UiThread; import android.support.design.widget.Snackbar; +import android.support.v4.view.GravityCompat; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.ActionMenuView; @@ -41,7 +42,9 @@ import android.support.v7.widget.DefaultItemAnimator; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; +import android.text.SpannableString; import android.util.Log; +import android.view.Gravity; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -107,6 +110,7 @@ import de.kuschku.util.instancestateutil.Storable; import de.kuschku.util.instancestateutil.Store; import de.kuschku.util.observables.AutoScroller; import de.kuschku.util.observables.lists.ObservableSortedList; +import de.kuschku.util.ui.MessageUtil; import static de.kuschku.util.AndroidAssert.assertNotNull; @@ -148,8 +152,10 @@ public class ChatActivity extends AppCompatActivity { private MessageAdapter messageAdapter; private AccountHeader accountHeader; private Drawer drawerLeft; + private Drawer drawerRight; private AdvancedEditor editor; private BufferViewConfigItem wrapper; + private NickListWrapper nicklistwrapper; @Nullable private QuasselService.LocalBinder binder; @Nullable @@ -499,6 +505,11 @@ public class ChatActivity extends AppCompatActivity { return true; } }); + drawerRight = new DrawerBuilder() + .withActivity(this) + .withSavedInstance(savedInstanceState) + .withDrawerGravity(Gravity.RIGHT) + .build(); } private void setupHeader(@Nullable Bundle savedInstanceState) { @@ -593,6 +604,12 @@ public class ChatActivity extends AppCompatActivity { messageAdapter.setMessageList(list); toolbar.setTitle(buffer.getName()); updateNoColor(buffer, formattingMenu.getMenu()); + + if (buffer instanceof ChannelBuffer && ((ChannelBuffer) buffer).getChannel() != null) { + nicklistwrapper = new NickListWrapper(drawerRight, ((ChannelBuffer) buffer).getChannel()); + } else { + drawerRight.removeAllItems(); + } } updateSubTitle(); } @@ -619,6 +636,7 @@ public class ChatActivity extends AppCompatActivity { } else { Snackbar.make(messages, "No buffer opened", Snackbar.LENGTH_LONG).show(); } + chatline.setVisibility(View.INVISIBLE); } public void onEventMainThread(@NonNull LoginRequireEvent event) { @@ -759,7 +777,7 @@ public class ChatActivity extends AppCompatActivity { private void updateSubTitle() { if (context.client() != null) { - String subtitle; + CharSequence subtitle; if (context.client().connectionStatus() == ConnectionChangeEvent.Status.CONNECTED) { if (status.bufferId >= 0) { Buffer buffer = context.client().bufferManager().buffer(status.bufferId); @@ -773,7 +791,11 @@ public class ChatActivity extends AppCompatActivity { } else if (buffer instanceof ChannelBuffer) { QIrcChannel channel = ((ChannelBuffer) buffer).getChannel(); if (channel != null) - subtitle = channel.topic(); + subtitle = MessageUtil.parseStyleCodes( + context.themeUtil(), + channel.topic(), + context.settings().mircColors.or(true) + ); else subtitle = ""; } diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/NickListWrapper.java b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/NickListWrapper.java new file mode 100644 index 0000000000000000000000000000000000000000..65a12afdc7c4ba0bc1c8b9452f4999081c13ee36 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/NickListWrapper.java @@ -0,0 +1,119 @@ +/* + * QuasselDroid - Quassel client for Android + * Copyright (C) 2016 Janne Koschinski + * Copyright (C) 2016 Ken Børge Viktil + * Copyright (C) 2016 Magnus Fjell + * Copyright (C) 2016 Martin Sandsmark <martin.sandsmark@kde.org> + * + * 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 de.kuschku.quasseldroid_ng.ui.chat; + +import com.mikepenz.materialdrawer.Drawer; +import com.mikepenz.materialdrawer.holder.BadgeStyle; +import com.mikepenz.materialdrawer.model.PrimaryDrawerItem; +import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem; + +import de.kuschku.libquassel.syncables.types.interfaces.QIrcChannel; +import de.kuschku.libquassel.syncables.types.interfaces.QIrcUser; +import de.kuschku.util.backports.Objects; +import de.kuschku.util.observables.callbacks.UICallback; +import de.kuschku.util.observables.lists.ObservableSortedList; + +public class NickListWrapper { + private final QIrcChannel channel; + private ObservableSortedList<QIrcUser> list = new ObservableSortedList<>(QIrcUser.class, new ObservableSortedList.ItemComparator<QIrcUser>() { + @Override + public int compare(QIrcUser o1, QIrcUser o2) { + int indexa = channel.network().modeToIndex(channel.userModes(o1)); + int indexb = channel.network().modeToIndex(channel.userModes(o2)); + if (indexa == indexb) { + return o1.nick().compareToIgnoreCase(o2.nick()); + } else { + return indexa - indexb; + } + } + + @Override + public boolean areContentsTheSame(QIrcUser oldItem, QIrcUser newItem) { + return oldItem == newItem; + } + + @Override + public boolean areItemsTheSame(QIrcUser item1, QIrcUser item2) { + return Objects.equals(item1.hostmask(), item2.hostmask()); + } + }); + + public NickListWrapper(Drawer drawerRight, QIrcChannel channel) { + drawerRight.removeAllItems(); + this.channel = channel; + this.list.addAll(channel.users()); + this.list.addCallback(new UICallback() { + @Override + public void notifyItemInserted(int position) { + drawerRight.getItemAdapter().add(position, fromUser(list.get(position))); + } + + @Override + public void notifyItemChanged(int position) { + drawerRight.getItemAdapter().notifyItemChanged(position); + } + + @Override + public void notifyItemRemoved(int position) { + drawerRight.removeItemByPosition(position); + } + + @Override + public void notifyItemMoved(int from, int to) { + notifyItemRemoved(from); + notifyItemInserted(from); + } + + @Override + public void notifyItemRangeInserted(int position, int count) { + for (int i = position; i < position + count; i++) { + notifyItemInserted(i); + } + } + + @Override + public void notifyItemRangeChanged(int position, int count) { + for (int i = position; i < position + count; i++) { + notifyItemChanged(i); + } + } + + @Override + public void notifyItemRangeRemoved(int position, int count) { + for (int i = position; i < position + count; i++) { + notifyItemRemoved(i); + } + } + }); + for (QIrcUser user : list) { + if (user != null) + drawerRight.getItemAdapter().add(fromUser(user)); + } + } + + private IDrawerItem fromUser(QIrcUser user) { + return new PrimaryDrawerItem() + .withName(user.nick()) + .withBadge(channel.network().modeToPrefix(channel.userModes(user))) + .withBadgeStyle(new BadgeStyle().withColor(0xFFFF0000).withTextColor(0xFFFFFFFF)); + } +} diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/drawer/BufferItem.java b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/drawer/BufferItem.java index d2882f7a3796f6fbca813ac0d3bc4e73d8f04bb3..6fa5b285f444f45c2620bff4540999ca85c65500 100644 --- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/drawer/BufferItem.java +++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/drawer/BufferItem.java @@ -123,7 +123,7 @@ public class BufferItem extends SecondaryDrawerItem { @Override public ColorHolder getTextColor() { int type = context.client().bufferSyncer().activity(buffer.getInfo().id()); - if ((type & Message.Type.Plain.value | type & Message.Type.Notice.value) != 0) + if ((type & Message.Type.Plain.value) != 0 || (type & Message.Type.Notice.value) != 0) return ColorHolder.fromColor(context.themeUtil().res.colorTintMessage); else if ((type & ~Message.Type.DayChange.value) != 0) return ColorHolder.fromColor(context.themeUtil().res.colorTintActivity); diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/drawer/NetworkItem.java b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/drawer/NetworkItem.java index 6192802965b6d716e184a60e5124f3f0e1487f98..b4ea95b7f65c7a37f0d1aa333f2f9db8b065cdc4 100644 --- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/drawer/NetworkItem.java +++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/drawer/NetworkItem.java @@ -109,7 +109,7 @@ public class NetworkItem extends PrimaryDrawerItem implements IObservable<Drawer @Override public int compareTo(@NonNull NetworkItem another) { - return network.networkName().compareTo(another.network.networkName()); + return network.networkName().compareToIgnoreCase(another.network.networkName()); } public QNetwork getNetwork() { diff --git a/app/src/main/java/de/kuschku/util/CompatibilityUtils.java b/app/src/main/java/de/kuschku/util/CompatibilityUtils.java index ab0523c1e29a7e666e1495e45b328c173e37e84c..ec83d84b1cf5051af32ba0131c35d8f61c920875 100644 --- a/app/src/main/java/de/kuschku/util/CompatibilityUtils.java +++ b/app/src/main/java/de/kuschku/util/CompatibilityUtils.java @@ -106,4 +106,17 @@ public class CompatibilityUtils { } return defaultValue; } + + /** + * Because Android’s String::split is broken + * @param str The string to be broken into chars + * @return A list with all substrings of length 1 of the first string, in order + */ + public static String[] partStringByChar(String str) { + String[] chars = new String[str.length()]; + for (int i = 0; i < chars.length; i++) { + chars[i] = str.substring(i, i+1); + } + return chars; + } } diff --git a/app/src/main/java/de/kuschku/util/irc/ModeUtils.java b/app/src/main/java/de/kuschku/util/irc/ModeUtils.java index 87fa50827f118bc22ae2515280a74e91592ee13e..048ee6f5d5ae3e56a34db7bab0f986f15a00f64b 100644 --- a/app/src/main/java/de/kuschku/util/irc/ModeUtils.java +++ b/app/src/main/java/de/kuschku/util/irc/ModeUtils.java @@ -27,13 +27,15 @@ import android.support.annotation.Nullable; import java.util.HashSet; import java.util.Set; +import de.kuschku.util.CompatibilityUtils; + public class ModeUtils { @NonNull public static Set<Character> toModes(@Nullable String modes) { Set<Character> modeSet = new HashSet<>(); if (modes == null) return modeSet; - for (String mode : modes.split("")) { + for (String mode : CompatibilityUtils.partStringByChar(modes)) { if (mode.length() == 1) modeSet.add(toMode(mode)); } diff --git a/app/src/main/java/de/kuschku/util/observables/lists/ObservableSet.java b/app/src/main/java/de/kuschku/util/observables/lists/ObservableSet.java index 0f7b99e57d4d933ac2146294aa96806155568639..e67bbeb226f7a769b5fb5aea3ab87c4ef7334c89 100644 --- a/app/src/main/java/de/kuschku/util/observables/lists/ObservableSet.java +++ b/app/src/main/java/de/kuschku/util/observables/lists/ObservableSet.java @@ -33,7 +33,7 @@ import de.kuschku.util.observables.callbacks.wrappers.MultiElementCallbackWrappe import static de.kuschku.util.AndroidAssert.assertNotNull; @SuppressWarnings("unchecked") -public class ObservableSet<T> extends HashSet<T> implements IObservableSet<ElementCallback<T>, T> { +public class ObservableSet<T> extends HashSet<T> implements IObservableSet<ElementCallback<T>, T>, ElementCallback<T> { @NonNull private final MultiElementCallbackWrapper<T> callback = MultiElementCallbackWrapper.<T>of(); @@ -105,6 +105,21 @@ public class ObservableSet<T> extends HashSet<T> implements IObservableSet<Eleme return new CallbackedArrayListIterator<>(super.iterator()); } + @Override + public void notifyItemInserted(T element) { + callback.notifyItemInserted(element); + } + + @Override + public void notifyItemRemoved(T element) { + callback.notifyItemRemoved(element); + } + + @Override + public void notifyItemChanged(T element) { + callback.notifyItemChanged(element); + } + class CallbackedArrayListIterator<E> implements Iterator<E> { final Iterator<E> iterator; E current;