Initial commit

This commit is contained in:
miruka
2019-03-21 23:28:14 -04:00
commit 0434c13cf9
33 changed files with 1880 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
# Copyright 2019 miruka
# This file is part of harmonyqml, licensed under GPLv3.
from .dummy import DummyBackend

View File

@@ -0,0 +1,89 @@
# Copyright 2019 miruka
# This file is part of harmonyqml, licensed under GPLv3.
import hashlib
from typing import Any, DefaultDict, Dict, NamedTuple, Optional
from PyQt5.QtCore import QDateTime, QObject, pyqtProperty, pyqtSlot
from .enums import Activity, MessageKind, Presence
from .list_model import ListModel, _QtListModel
class User(NamedTuple):
user_id: str
display_name: str
avatar_url: Optional[str] = None
class Room(NamedTuple):
account_id: str
room_id: str
display_name: str
subtitle: str = ""
unread_messages: int = 0
presence: Presence = Presence.none
activity: Activity = Activity.none
last_activity_timestamp_ms: Optional[int] = None
avatar_url: Optional[str] = None
class Message(NamedTuple):
sender_id: str
date_time: QDateTime
content: str
kind: MessageKind = MessageKind.text
sender_avatar: Optional[str] = None
class Backend(QObject):
def __init__(self) -> None:
super().__init__()
self._known_users: Dict[str, User] = {}
self.rooms: ListModel = ListModel()
self.messages: DefaultDict[str, ListModel] = DefaultDict(ListModel)
@pyqtProperty(_QtListModel, constant=True)
def roomsModel(self) -> _QtListModel:
return self.rooms.qt_model
@pyqtProperty("QVariantMap", constant=True)
def messagesModel(self) -> Dict[str, _QtListModel]:
return {room_id: l.qt_model for room_id, l in self.messages.items()}
@pyqtSlot(str, str, str)
def sendMessage(self, sender_id: str, room_id: str, markdown: str) -> None:
self.localEcho(sender_id, room_id, markdown)
self.sendToServer(sender_id, room_id, markdown)
def localEcho(self, sender_id: str, room_id: str, html: str) -> None:
self.messages[room_id].append(Message(
sender_id, QDateTime.currentDateTime(), html,
))
def sendToServer(self, sender_id: str, room_id: str, html: str) -> None:
pass
@pyqtSlot(str, result="QVariantMap")
def getUser(self, user_id: str) -> Dict[str, Any]:
try:
return self._known_users[user_id]._asdict()
except KeyError:
name = user_id.lstrip("@").split(":")[0].capitalize()
user = User(user_id, name)
self._known_users[user_id] = user
return user._asdict()
@pyqtSlot(str, result=float)
def hueFromString(self, string: str) -> float:
# pylint: disable=no-self-use
md5 = hashlib.md5(bytes(string, "utf-8")).hexdigest()
return float("0.%s" % int(md5[-10:], 16))

View File

@@ -0,0 +1,57 @@
# Copyright 2019 miruka
# This file is part of harmonyqml, licensed under GPLv3.
from PyQt5.QtCore import QDateTime, Qt
from .base import Backend, Message, Room
class DummyBackend(Backend):
def __init__(self) -> None:
super().__init__()
dt = lambda t: QDateTime.fromString(f"2019-03-19T{t}.123",
Qt.ISODateWithMs)
db = lambda t: QDateTime.fromString(f"2019-03-20T{t}.456",
Qt.ISODateWithMs)
self.rooms.extend([
Room("@renko:matrix.org", "!test:matrix.org", "Test", "Test room"),
Room("@renko:matrix.org", "!mary:matrix.org", "Mary",
"Lorem ipsum sit dolor amet", 2),
Room("@renko:matrix.org", "!foo:matrix.org", "Another room"),
Room("@mary:matrix.org", "!test:matrix.org", "Test", "Test room"),
Room("@mary:matrix.org", "!mary:matrix.org", "Renko",
"Lorem ipsum sit dolor amet"),
])
self.messages["!test:matrix.org"].extend([
Message("@renko:matrix.org", dt("10:20:13"), "Lorem"),
Message("@renko:matrix.org", dt("10:22:01"), "Ipsum"),
Message("@renko:matrix.org", dt("10:22:50"), "Combine"),
Message("@renko:matrix.org", dt("10:30:41"),
"Time passed, don't combine"),
Message("@mary:matrix.org", dt("10:31:12"),
"Different person, don't combine"),
Message("@mary:matrix.org", dt("10:32:04"),
"But combine me"),
Message("@mary:matrix.org", dt("13:10:20"),
"Long time passed, conv break"),
Message("@renko:matrix.org", db("10:22:01"), "Daybreak"),
Message("@mary:matrix.org", db("10:22:03"),
"A longer message to test text wrapping. "
"Lorem ipsum dolor sit amet, consectetuer adipiscing "
"elit. Aenean commodo ligula "
"eget dolor. Aenean massa. Cem sociis natoque penaibs "
"et magnis dis parturient montes, nascetur ridiculus "
"mus. Donec quam. "),
])
self.messages["!mary:matrix.org"].extend([
Message("@mary:matrix.org", dt("10:22:23"), "First"),
Message("@mary:matrix.org", dt("12:24:10"), "Second"),
])
self.messages["!foo:matrix.org"].extend([])

View File

@@ -0,0 +1,31 @@
# Copyright 2019 miruka
# This file is part of harmonyqml, licensed under GPLv3.
from enum import Enum
class Activity(Enum):
none = 0
focus = 1
paused_typing = 2
typing = 3
class Presence(Enum):
none = 0
offline = 1
invisible = 2
away = 3
busy = 4
online = 5
class MessageKind(Enum):
audio = "m.audio"
emote = "m.emote"
file = "m.file"
image = "m.image"
location = "m.location"
notice = "m.notice"
text = "m.text"
video = "m.video"

View File

@@ -0,0 +1,191 @@
import logging
from collections.abc import MutableSequence
from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple, Union
from namedlist import namedlist
from PyQt5.QtCore import (
QAbstractListModel, QModelIndex, Qt, pyqtProperty, pyqtSlot
)
NewValue = Union[Mapping[str, Any], Sequence]
class _QtListModel(QAbstractListModel):
def __init__(self) -> None:
super().__init__()
self._ref_namedlist = None
self._roles: Tuple[str, ...] = ()
self._list: list = []
def roleNames(self) -> Dict[int, bytes]:
return {Qt.UserRole + i: bytes(f, "utf-8")
for i, f in enumerate(self._roles, 1)}
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
if role <= Qt.UserRole:
return None
return self._list[index.row()][role - Qt.UserRole - 1]
def rowCount(self, _: QModelIndex = QModelIndex()) -> int:
return len(self._list)
def _convert_new_value(self, value: NewValue) -> Any:
if isinstance(value, Mapping):
if not self._ref_namedlist:
self._ref_namedlist = namedlist("ListItem", value.keys())
self._roles = tuple(value.keys())
return self._ref_namedlist(**value) # type: ignore
if isinstance(value, Sequence):
if not self._ref_namedlist:
try:
self._ref_namedlist = namedlist(
value.__class__.__name__, value._fields # type: ignore
)
self._roles = tuple(value._fields) # type: ignore
except AttributeError:
raise TypeError(
"Need a mapping/dict, namedtuple or namedlist as "
"first value to set allowed keys/fields."
)
return self._ref_namedlist(*value) # type: ignore
raise TypeError("Value must be a mapping or sequence.")
@pyqtSlot(int, result="QVariantMap")
def get(self, index: int) -> Dict[str, Any]:
return self._list[index]._asdict()
@pyqtSlot(int, list)
def insert(self, index: int, value: NewValue) -> None:
value = self._convert_new_value(value)
self.beginInsertRows(QModelIndex(), index, index)
self._list.insert(index, value)
self.endInsertRows()
@pyqtProperty(int)
def count(self) -> int:
return self.rowCount()
@pyqtSlot(list)
def append(self, value: NewValue) -> None:
self.insert(self.rowCount(), value)
@pyqtSlot(int, list)
def set(self, index: int, value: NewValue) -> None:
qidx = self.index(index, 0)
value = self._convert_new_value(value)
self._list[index] = value
self.dataChanged.emit(qidx, qidx, self.roleNames())
@pyqtSlot(int, str, "QVariant")
def setProperty(self, index: int, prop: str, value: Any) -> None:
self._list[index][self._roles.index(prop)] = value
qidx = self.index(index, 0)
self.dataChanged.emit(qidx, qidx, self.roleNames())
# pylint: disable=invalid-name
@pyqtSlot(int, int)
@pyqtSlot(int, int, int)
def move(self, from_: int, to: int, n: int = 1) -> None:
qlast = from_ + n - 1
if (n <= 0) or (from_ == to) or (qlast == to) or \
not (self.rowCount() > qlast >= 0) or \
not self.rowCount() >= to >= 0:
logging.warning("No need for move or out of range")
return
qidx = QModelIndex()
qto = min(self.rowCount(), to + n if to > from_ else to)
# print(f"self.beginMoveRows(qidx, {from_}, {qlast}, qidx, {qto})")
valid = self.beginMoveRows(qidx, from_, qlast, qidx, qto)
if not valid:
logging.warning("Invalid move operation")
return
last = from_ + n
cut = self._list[from_:last]
del self._list[from_:last]
self._list[to:to] = cut
self.endMoveRows()
@pyqtSlot(int)
def remove(self, index: int) -> None:
self.beginRemoveRows(QModelIndex(), index, index)
del self._list[index]
self.endRemoveRows()
@pyqtSlot()
def clear(self) -> None:
# Reimplemented for performance reasons (begin/endRemoveRows)
self.beginRemoveRows(QModelIndex(), 0, self.rowCount())
self._list.clear()
self.endRemoveRows()
class ListModel(MutableSequence):
def __init__(self, initial_data: Optional[List[NewValue]] = None) -> None:
super().__init__()
self.qt_model = _QtListModel()
if initial_data:
self.extend(initial_data)
def __repr__(self) -> str:
return "%s[%s]" % (type(self).__name__,
", ".join((repr(i) for i in self)))
def __getitem__(self, index):
# pylint: disable=protected-access
return self.qt_model._list[index]
def __setitem__(self, index, value) -> None:
self.qt_model.set(index, value)
def __delitem__(self, index) -> None:
self.qt_model.remove(index)
def __len__(self) -> int:
return self.qt_model.rowCount()
def insert(self, index: int, value: NewValue) -> None:
self.qt_model.insert(index, value)
def setProperty(self, index: int, prop: str, value: Any) -> None:
"Set role of the item at *index* to *value*."
self.qt_model.setProperty(index, prop, value)
# pylint: disable=invalid-name
def move(self, from_: int, to: int, n: int = 1) -> None:
"Move *n* items *from_* index *to* another."
self.qt_model.move(from_, to, n)
def clear(self) -> None:
self.qt_model.clear()