/*
* Kontalk Java client
* Copyright (C) 2016 Kontalk Devteam <devteam@kontalk.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 org.kontalk.model.chat;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;
import java.util.Optional;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.kontalk.model.Contact;
import org.kontalk.model.Model;
import org.kontalk.persistence.Database;
/**
* The global list of all chats.
* @author Alexander Bikadorov {@literal <bikaejkb@mail.tu-berlin.de>}
*/
public final class ChatList extends Observable implements Observer, Iterable<Chat> {
private static final Logger LOGGER = Logger.getLogger(ChatList.class.getName());
public enum ViewChange {
MODIFIED, UNREAD
}
private final Set<Chat> mChats = Collections.synchronizedSet(new HashSet<Chat>());
private boolean mUnread = false;
public void load(Map<Integer, Contact> contactMap) {
assert mChats.isEmpty();
Database db = Model.database();
try (ResultSet chatRS = db.execSelectAll(Chat.TABLE)) {
while (chatRS.next()) {
Chat chat = Chat.load(db, chatRS, contactMap).orElse(null);
if (chat == null)
continue;
this.putSilent(chat);
mUnread |= !chat.isRead();
}
} catch (SQLException ex) {
LOGGER.log(Level.WARNING, "can't load chats from db", ex);
}
this.changed(ViewChange.MODIFIED);
}
public Set<Chat> getAll() {
return Collections.unmodifiableSet(mChats);
}
/** Get single chat with contact and XMPPID. */
public Optional<SingleChat> get(Contact contact, String xmmpThreadID) {
synchronized(mChats) {
return mChats.stream()
.filter(chat -> chat instanceof SingleChat)
.map(chat -> (SingleChat) chat)
.filter(chat -> chat.getXMPPID().equals(xmmpThreadID)
&& chat.getMember().getContact().equals(contact))
.findFirst();
}
}
public Optional<GroupChat> get(GroupMetaData gData) {
synchronized(mChats) {
return mChats.stream()
.filter(chat -> chat instanceof GroupChat)
.map(chat -> (GroupChat) chat)
.filter(chat -> chat.getGroupData().equals(gData))
.findFirst();
}
}
public SingleChat getOrCreate(Contact contact) {
return this.getOrCreate(contact, "");
}
/** Find single chat for contact and XMPP ID or creates a new chat. */
public SingleChat getOrCreate(Contact contact, String xmppThreadID) {
SingleChat chat = this.get(contact, xmppThreadID).orElse(null);
if (chat != null)
return chat;
return this.createNew(contact, xmppThreadID);
}
private SingleChat createNew(Contact contact, String xmppThreadID) {
SingleChat newChat = new SingleChat(contact, xmppThreadID);
LOGGER.config("new single chat: "+newChat);
this.putSilent(newChat);
this.changed(ViewChange.MODIFIED);
return newChat;
}
public GroupChat create(List<ProtoMember> members, GroupMetaData gData) {
return createNew(members, gData, "");
}
public GroupChat createNew(List<ProtoMember> members, GroupMetaData gData, String subject) {
GroupChat newChat = GroupChat.create(members, gData, subject);
LOGGER.config("new group chat: "+newChat);
this.putSilent(newChat);
this.changed(ViewChange.MODIFIED);
return newChat;
}
private void putSilent(Chat chat) {
boolean succ = mChats.add(chat);
if (!succ) {
LOGGER.warning("chat already in chat list: "+chat);
return;
}
chat.addObserver(this);
}
public boolean contains(Contact contact) {
return this.get(contact, "").isPresent();
}
public boolean isEmpty() {
return mChats.isEmpty();
}
public void delete(Chat chat) {
boolean succ = mChats.remove(chat);
if (!succ) {
LOGGER.warning("can't delete chat, not found: "+chat);
return;
}
chat.delete();
chat.deleteObservers();
this.changed(ViewChange.MODIFIED);
}
/** Return if any chat is unread. */
public boolean isUnread() {
return mUnread;
}
private void changed(ViewChange change) {
this.setChanged();
this.notifyObservers(change);
}
@Override
public void update(Observable o, Object arg) {
if (arg != Chat.ViewChange.READ || !(o instanceof Chat))
return;
boolean unread = !((Chat) o).isRead();
if (mUnread == unread)
return;
if (!unread) {
// one chat was read, are there still any unread chats?
synchronized(mChats) {
if (mChats.stream().anyMatch(chat -> !chat.isRead()))
return;
}
}
mUnread = !mUnread;
this.changed(ViewChange.UNREAD);
}
@Override
public Iterator<Chat> iterator() {
return mChats.iterator();
}
}