Pleroma chat client in PyQt5
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
plchat/plchat.py

1586 lines
60 KiB

#PlChat is a pleroma chat client
# Copyright (C) 2021 Knott Eye
#
# 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 <https://www.gnu.org/licenses/>.
import platform, keyring
os = platform.system()
if os == "Windows":
import keyring.backends.Windows
keyring.set_keyring(keyring.backends.Windows.WinVaultKeyring())
elif os == "Darwin":
import keyring.backends.OS_X
keyring.set_keyring(keyring.backends.OS_X.Keyring())
else:
# Probably linux or BSD so we'll just go for it
import keyring.backends.SecretService
keyring.set_keyring(keyring.backends.SecretService.Keyring())
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from notifypy import Notify
import sys, threading, queue, asyncio, urllib, appdirs, os, time, pleroma, re, magic, monkeypatch, requests, misc, json, timeconvert, videowidget, audiowidget
CACHE = appdirs.AppDirs('plchat', 'plchat').user_cache_dir
APPDATA = appdirs.AppDirs('plchat', 'plchat').user_data_dir
THREADS = {}
STATIC_PREF = ''
ICON_PATH = os.path.join(os.path.dirname(__file__), "fedi.svg")
ICON_PATH_COLOR = os.path.join(os.path.dirname(__file__), "fedi_color.svg")
NOTIF_SOUND = os.path.join(os.path.dirname(__file__), 'notif.wav')
class App(QMainWindow):
settings = QSettings(APPDATA+"/settings.ini")
_eventloop = asyncio.new_event_loop()
accts = {}
def __init__(self, processEvents):
super().__init__()
self.title = 'PlChat'
self.processEvents = processEvents
self.initUI()
def initUI(self):
self.setWindowTitle(self.title)
if self.settings.value('colorIcon', type=bool):
self.setWindowIcon(QIcon(QPixmap(ICON_PATH_COLOR)))
else:
self.setWindowIcon(QIcon(QPixmap(ICON_PATH)))
self.setGeometry(self.settings.value('left', type=int) or 10, self.settings.value('top', type=int) or 10, self.settings.value('width', type=int) or 640, self.settings.value('height', type=int) or 480)
if self.settings.value('silenceNotifications', type=bool):
Notification._notification_audio = None
self._exit = False
self.totpReady = False
self.Err = QErrorMessage()
exitAction = QAction('&Exit', self)
exitAction.setShortcut('Ctrl+Q')
exitAction.setToolTip('Exit application')
exitAction.triggered.connect(self.exitActionTrigger)
newChatAction = QAction('New Chat', self, triggered=self.newChatDialog)
newChatAction.setShortcut('Ctrl+N')
newChatAction.setToolTip('Add a new chat')
closeChatAction = QAction('Close Chat', self, triggered=self.closeTab)
closeChatAction.setShortcut('Ctrl+W')
closeChatAction.setToolTip('Close the current chat tab')
newAcctAction = QAction('New Account', self, triggered=self.newAcctDialog)
newAcctAction.setShortcut('Ctrl+Shift+N')
newAcctAction.setToolTip('Add a new account')
logoutAction = QAction('Logout', self, triggered=self.logout)
logoutAction.setToolTip('Log out of the current account')
reopenAction = QAction('Reopen All Chats', self, triggered=self.reopenAll)
reopenAction.setToolTip('In case something breaks')
contactAction = QAction('Contact', self, triggered=self.contactDialog)
licenseAction = QAction('License', self, triggered=self.licenseDialog)
self.closeToTrayAction = QAction('Close To System Tray', self, checkable=True)
self.closeToTrayAction.setChecked(self.settings.value('closeToTray', type=bool))
self.openInTrayAction = QAction('Open In System Tray', self, checkable=True)
self.openInTrayAction.setChecked(self.settings.value('openInTray', type=bool))
self.openInTrayAction.setToolTip('Does nothing if not also closing to tray.')
self.animatePicturesAction = QAction('Display Animations', self, checkable=True)
self.animatePicturesAction.setChecked(self.settings.value('animatePictures', type=bool))
self.animatePicturesAction.changed.connect(updateAnimationPref)
self.fetchBackgroundsAction = QAction("Fetch Backgrounds", self, checkable=True)
self.fetchBackgroundsAction.setChecked(self.settings.value('fetchBackgrounds', type=bool))
self.fetchBackgroundsAction.setToolTip("Most instances do not set this in a way that PlChat can check for. It will (probably) not work like you expect, but there is nothing I can do about it.")
self.darkModeAction = QAction("Dark Mode", self, checkable=True)
self.darkModeAction.setChecked(self.settings.value('darkMode', type=bool))
self.darkModeAction.setToolTip("Only affects chat bubbles")
self.colorIconAction = QAction("Use Color Icon", self, checkable=True, triggered=self.useColorIcon)
self.colorIconAction.setChecked(self.settings.value('colorIcon', type=bool))
self.sendNotificationsAction = QAction("Send Notifications", self, checkable=True)
self.sendNotificationsAction.setChecked(self.settings.value('sendNotifications', type=bool))
self.silenceNotifsAction = QAction("Silence Notifications", self, checkable=True, triggered=self.silenceNotifs)
self.silenceNotifsAction.setChecked(self.settings.value('silenceNotifications', type=bool))
self.fetchHeadersAction = QAction("Fetch Headers", self, checkable=True)
self.fetchHeadersAction.setChecked(self.settings.value('fetchHeaders', type=bool))
self.twoFourTimeAction = QAction("Display 24-Hour Time", self, checkable=True)
self.twoFourTimeAction.setChecked(self.settings.value('twoFourTime', type=bool))
self.acctComboBox = QComboBox(self)
self.acctComboBox.currentIndexChanged.connect(self.acctSwitch)
acctComboBoxAction = QWidgetAction(self)
acctComboBoxAction.setDefaultWidget(self.acctComboBox)
self.acctComboBox.show()
menubar = self.menuBar()
if self.closeToTrayAction.isChecked():
systraymenu = QMenu()
hideAction = QAction("Hide", self, triggered=self.hide)
showAction = QAction("Show", self, triggered=self.show)
systraymenu.addAction(showAction)
systraymenu.addAction(hideAction)
systraymenu.addAction(self.silenceNotifsAction)
systraymenu.addAction(exitAction)
self.trayIcon = QSystemTrayIcon()
if self.settings.value('colorIcon', type=bool):
self.trayIcon.setIcon(QIcon(ICON_PATH_COLOR))
else:
self.trayIcon.setIcon(QIcon(ICON_PATH))
self.trayIcon.setVisible(True)
self.trayIcon.setToolTip("PlChat")
self.trayIcon.setContextMenu(systraymenu)
self.trayIcon.activated.connect(self.systrayClicked)
else:
self.trayIcon = None
filemenu = menubar.addMenu("File")
filemenu.addAction(newChatAction)
filemenu.addAction(closeChatAction)
filemenu.addAction(newAcctAction)
filemenu.addAction(exitAction)
editmenu = menubar.addMenu("Edit")
editmenu.addAction(reopenAction)
editmenu.setToolTipsVisible(True)
prefsmenu = editmenu.addMenu("Preferences")
prefsmenu.addAction(self.closeToTrayAction)
prefsmenu.addAction(self.openInTrayAction)
prefsmenu.addAction(self.animatePicturesAction)
prefsmenu.addAction(self.darkModeAction)
prefsmenu.addAction(self.colorIconAction)
prefsmenu.addAction(self.sendNotificationsAction)
prefsmenu.addAction(self.silenceNotifsAction)
prefsmenu.addAction(self.fetchBackgroundsAction)
prefsmenu.addAction(self.fetchHeadersAction)
prefsmenu.addAction(self.twoFourTimeAction)
prefsmenu.setToolTipsVisible(True)
accountmenu = menubar.addMenu('Accounts')
accountmenu.addAction(acctComboBoxAction)
accountmenu.addSeparator()
accountmenu.addAction(logoutAction)
aboutmenu = menubar.addMenu('About')
aboutmenu.addAction(contactAction)
aboutmenu.addAction(licenseAction)
self.tabs = QTabWidget()
self.tabs.setMovable(True)
self.tabs.setTabsClosable(True)
self.tabs.tabCloseRequested.connect(self.closeTab)
self.tabs.currentChanged.connect(self.changeTab)
self.setFocusProxy(self.tabs)
self.setCentralWidget(self.tabs)
if not self.openInTrayAction.isChecked() or not self.closeToTrayAction.isChecked():
self.show()
self.HeaderFont = QFont()
self.HeaderFont.setPointSize(16)
self.headerEmojiFontSize = QFontMetrics(self.HeaderFont).height()
self.defaultFontMetrics = QFontMetrics(QFont())
self.emojiFontSize = self.defaultFontMetrics.height()
self.TimestampFont = QFont()
self.TimestampFont.setPointSize(7)
self.installEventFilter(self)
asyncio.set_event_loop(self._eventloop)
self._eventloop.call_soon(self.eventLoop)
acctList = self.settings.value('acctList', type=list)
if not acctList:
self.newAcctDialog()
return
for ind in range(0,len(acctList)):
CallThread(getAvi, None, acctList[ind]['instance'])
try:
self.initAcct(acctList[ind]['instance'], acctList[ind]['username'])
except:
print("account info corrupted, deleting")
del acctList[ind]
if acctList:
self.settings.setValue('acctList', acctList)
def systrayClicked(self, reason):
if reason == QSystemTrayIcon.Trigger:
if self.isVisible():
self.hide()
else:
self.show()
def silenceNotifs(self, dothing):
if dothing:
# Definitely not supposed to be poking around in the internals like this lul
Notification._notification_audio = None
else:
Notification.audio = NOTIF_SOUND
def useColorIcon(self, dothing):
if dothing:
self.setWindowIcon(QIcon(QPixmap(ICON_PATH_COLOR)))
if self.trayIcon:
self.trayIcon.setIcon(QIcon(ICON_PATH_COLOR))
else:
self.setWindowIcon(QIcon(QPixmap(ICON_PATH)))
if self.trayIcon:
self.trayIcon.setIcon(QIcon(ICON_PATH))
def eventLoop(self):
# Custom event loop to process queue events
self.processEvents()
self._eventloop.call_soon(self.eventLoop)
monkeypatch.sleep(0.01)
def exitActionTrigger(self):
self._exit = True
self.close()
def closeEvent(self, event):
if not self._exit and self.closeToTrayAction.isChecked():
self.hide()
return
self.settings.setValue("left", self.x())
self.settings.setValue("top", self.y())
self.settings.setValue("width", self.width())
self.settings.setValue("height", self.height())
self.settings.setValue("closeToTray", self.closeToTrayAction.isChecked())
self.settings.setValue("openInTray", self.openInTrayAction.isChecked())
self.settings.setValue("animatePictures", self.animatePicturesAction.isChecked())
self.settings.setValue("darkMode", self.darkModeAction.isChecked())
self.settings.setValue("fetchBackgrounds", self.fetchBackgroundsAction.isChecked())
self.settings.setValue("sendNotifications", self.sendNotificationsAction.isChecked())
self.settings.setValue("fetchHeaders", self.fetchHeadersAction.isChecked())
self.settings.setValue("twoFourTime", self.twoFourTimeAction.isChecked())
self.settings.setValue('colorIcon', self.colorIconAction.isChecked())
self.settings.setValue('silenceNotifications', self.silenceNotifsAction.isChecked())
event.accept()
self._eventloop.stop()
def getCurrentAcc(self):
if self.acctComboBox.currentIndex() == -1 or not self.accts:
return (False, False)
# Returns username, instance
return self.acctComboBox.currentText().split('@')[1], self.acctComboBox.currentText().split('@')[2]
def badLogin(self, name):
self.Err.showMessage("Bad login info for: "+name)
self.newAcctDialog()
def newAcctDialog(self):
dialog = LoginDialog(self)
dialog.getInput(self.initAcct)
def newChatDialog(self):
u, i = self.getCurrentAcc()
text, ok = QInputDialog.getText(self, 'Start A New Chat', "Username:", QLineEdit.Normal, "")
if ok:
if len(text) < 1:
self.Err.showMessage("No text provided")
else:
if text[0] == '@':
text = text[1:]
if text.find('@'+i) != -1:
text = text.replace('@'+i, '')
CallThread(self.accts[u+i].getAcctInfo, self.newChatReady, text).start()
def newChatReady(self, result):
if result is None:
self.Err.showMessage("I couldn't find that user!")
elif not result['pleroma']['accepts_chat_messages']:
self.Err.showMessage("This user is on instance that does not support pleroma chats.\nIt may be too old, or it may not be pleroma.")
else:
u, i = self.getCurrentAcc()
closedList = self.settings.value('closed'+u+i, type=list) or []
for ind in range(0,(len(closedList))):
if closedList[ind] == result['acct']:
del closedList[ind]
break
self.settings.setValue('closed'+u+i, closedList)
CallThread(self.accts[u+i].addChat, self.populateChats, result['id']).start()
def getTotp(self, acct, challenge_type):
if type(challenge_type) != list:
challenge_type = [challenge_type, 'recovery']
self._eventloop.call_soon_threadsafe(self.makeTotpCard, '@'+acct.username+'@'+acct.instance, challenge_type)
while not self.totpReady:
monkeypatch.sleep(0.2)
t = self.totpReady
self.totpReady = None
return t
def makeTotpCard(self, acct, challenge_type):
dialog = TotpCard(acct, challenge_type)
dialog.getInput()
def contactDialog(self):
dialog = ContactCard(self)
dialog.show()
def licenseDialog(self):
dialog = LicenseCard(self)
dialog.show()
def reopenAll(self):
u, i = self.getCurrentAcc()
if not u or not i:
return
self.settings.setValue('closed'+u+i, [])
CallThread(self.accts[u+i].listChats, self.populateChats).start()
def initAcct(self, instance, username, password=None):
if password:
acct = pleroma.Account(instance, username, password, totpFunc=self.getTotp)
else:
token = keyring.get_password('plchat', instance+username+'access_token')
refresh_token = keyring.get_password('plchat', instance+username+'refresh_token')
clientID = keyring.get_password('plchat', instance+username+'clientID')
clientSecret = keyring.get_password('plchat', instance+username+'clientSecret')
acct = pleroma.Account(instance, username,
token=token,
refresh_token=refresh_token,
clientID=clientID,
clientSecret=clientSecret,
totpFunc=self.getTotp
)
RegisterThread(acct, self.doneRegister).start()
def doneRegister(self, acct):
self.accts[acct.username+acct.instance] = acct
self.acctComboBox.addItem('@'+acct.username+'@'+acct.instance)
acctList = self.settings.value('acctList', type=list) or []
concat = True
for acc in acctList:
if acc['username'] == acct.username and acc['instance'] == acct.instance:
concat = False
if concat:
acctList.append({"username": acct.username, "instance": acct.instance})
self.settings.setValue('acctList', acctList)
keyring.set_password('plchat', acct.instance+acct.username+'access_token', acct.token)
keyring.set_password('plchat', acct.instance+acct.username+'refresh_token', acct.refresh_token)
keyring.set_password('plchat', acct.instance+acct.username+'clientID', acct.clientID)
keyring.set_password('plchat', acct.instance+acct.username+'clientSecret', acct.clientSecret)
CallThread(getAvi, None, acct.instance).start()
CallThread(acct.getInstanceInfo, None).start()
def logout(self):
u, i = self.getCurrentAcc()
if not u or not i:
print("Couldn't log out")
return
self.acctComboBox.removeItem(self.acctComboBox.currentIndex())
del self.accts[u+i]
acctList = self.settings.value('acctList', type=list)
for j in range(len(acctList)):
if acctList[j]['username'] == u and acctList[j]['instance'] == i:
keyring.delete_password("plchat", i+u+'access_token')
keyring.delete_password("plchat", i+u+'refresh_token')
keyring.delete_password("plchat", i+u+'clientID')
keyring.delete_password("plchat", i+u+'clientSecret')
del acctList[j]
self.settings.setValue('acctList', acctList)
def acctSwitch(self):
u, i = self.getCurrentAcc()
if u+i in self.accts and 'title' in self.accts[u+i].getInstanceInfo():
self.setWindowTitle(self.accts[u+i].getInstanceInfo()['title']+' Chat')
else:
self.setWindowTitle('PlChat')
self.tabs.clear()
if u and i:
CallThread(self.accts[u+i].listChats, self.populateChats).start()
def populateChats(self, chatList):
if type(chatList) == dict:
chatList = [chatList]
u, i = self.getCurrentAcc()
if not u or not i:
return
closedList = self.settings.value('closed'+u+i, type=list) or []
for chat in chatList:
c = False
for ind in range(0, self.tabs.count()):
if self.tabs.widget(ind).acct == chat['account']['acct']:
c = True
for entry in closedList:
if chat['account']['acct'] == entry:
c = True
if c:
continue
ctab = ChatArea(chat, self.tabs)
self.tabs.addTab(ctab, chat['account']['display_name'])
def closeTab(self, index=None):
if not index:
index = self.tabs.currentIndex()
act = self.tabs.widget(index).acct
u, i = self.getCurrentAcc()
closedList = self.settings.value('closed'+u+i, type=list) or []
f = True
for entry in closedList:
if entry == act:
f = False
if f:
closedList.append(act)
self.settings.setValue('closed'+u+i, closedList)
self.tabs.removeTab(index)
def handlePleromaEvent(self, acct, event):
if event['event'] == 'pleroma:chat_update':
payload = json.loads(event['payload'])
tmp = -1
for ind in range(0, self.tabs.count()):
if self.tabs.widget(ind).acct == payload['account']['acct']:
self._eventloop.call_soon_threadsafe(self.tabs.widget(ind).addMessage, payload['last_message'])
tmp = ind
if payload['last_message']['account_id'] != acct.flakeid and (not self.hasFocus() or payload['account']['acct'] != self.tabs.widget(self.tabs.currentIndex()).acct):
if self.sendNotificationsAction.isChecked():
CallThread(self.makeNotification, None, payload['last_message']['content'], payload['account']['acct'], payload['account']['avatar_static']).start()
app.alert(self, 0)
if tmp > 0:
self._eventloop.call_soon_threadsafe(self.setUrgent, tmp)
else:
u, i = ex.getCurrentAcc()
tmp = self.tabs.count()
closedList = self.settings.value('closed'+u+i, type=list) or []
for ind in range(0,(len(closedList))):
if closedList[ind] == payload['account']['acct']:
del closedList[ind]
break
self.settings.setValue('closed'+u+i, closedList)
self._eventloop.call_soon_threadsafe(self.populateChats, payload)
while self.tabs.tabIcon(tmp).isNull():
time.sleep(0.3)
self._eventloop.call_soon_threadsafe(self.setUrgent, tmp)
def makeNotification(self, content, user, url):
path = getPic(url)
sendNotification(content, title=user, icon=path)
def setUrgent(self, ind):
icon = QIcon()
icon.addPixmap(QPixmap(os.path.join(os.path.dirname(__file__), "unread.svg")))
self.tabs.setTabIcon(ind, icon)
self.tabs.widget(ind).unread = True
if self.trayIcon:
self.trayIcon.setIcon(icon)
self.setWindowIcon(icon)
def changeTab(self, ind):
if not self.tabs.widget(ind):
return
if self.tabs.widget(ind).unread:
CallThread(getPic, self.tabs.widget(ind).useAvi, self.tabs.widget(ind).avaURL).start()
self.tabs.widget(ind).unread = False
flip = True
for A in range(0, self.tabs.count()):
if self.tabs.widget(A).unread:
flip = False
if flip:
if self.trayIcon:
if self.settings.value('colorIcon', type=bool):
self.trayIcon.setIcon(QIcon(ICON_PATH_COLOR))
else:
self.trayIcon.setIcon(QIcon(ICON_PATH))
if self.settings.value('colorIcon', type=bool):
self.setWindowIcon(QIcon(QPixmap(ICON_PATH_COLOR)))
else:
self.setWindowIcon(QIcon(QPixmap(ICON_PATH)))
self.tabs.widget(ind).markRead()
self.tabs.widget(ind).setFocus(Qt.NoFocusReason)
def eventFilter(self, object, event):
if event.type() == QEvent.WindowActivate:
self.windowActivate()
return False
def windowActivate(self):
ind = self.tabs.currentIndex()
if not self.tabs.widget(ind):
return
if self.tabs.widget(ind).unread:
CallThread(getPic, self.tabs.widget(ind).useAvi, self.tabs.widget(ind).avaURL).start()
self.tabs.widget(ind).unread = False
flip = True
for A in range(0, self.tabs.count()):
if self.tabs.widget(A).unread:
flip = False
if flip:
if self.trayIcon:
if self.settings.value('colorIcon', type=bool):
self.trayIcon.setIcon(QIcon(ICON_PATH_COLOR))
else:
self.trayIcon.setIcon(QIcon(ICON_PATH))
if self.settings.value('colorIcon', type=bool):
self.setWindowIcon(QIcon(QPixmap(ICON_PATH_COLOR)))
else:
self.setWindowIcon(QIcon(QPixmap(ICON_PATH)))
self.tabs.widget(ind).markRead()
self.tabs.widget(ind).setFocus(Qt.NoFocusReason)
class BGWidget(QWidget):
def __init__(self, path=None):
super().__init__()
self.path=path
self.pixmap = QPixmap()
def setPath(self, path):
self.path = path
def paintEvent(self, event):
super().paintEvent(event)
if self.pixmap.isNull() and self.path:
self.pixmap = QPixmap(self.path)
painter = QPainter(self)
if not self.pixmap.isNull():
self.pixmap = self.pixmap.scaled(self.width(), self.height(), Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation)
painter.drawPixmap(0,0, self.pixmap)
class ChatArea(QWidget):
def __init__(self, chat, parent=None):
super().__init__(parent=parent)
self.acct = chat['account']['acct']
self.parent = parent
self.chatID = chat['id']
self.account = chat['account']
self.unread = False
if ex.animatePicturesAction.isChecked():
avatar_prop = 'avatar'
header_prop = 'header'
else:
avatar_prop = 'avatar_static'
header_prop = 'header_static'
self.avaURL = chat['account'][avatar_prop]
CallThread(getPic, self.useAvi, self.avaURL).start()
if ex.fetchHeadersAction.isChecked():
CallThread(getPic, self.headerReady, chat['account'][header_prop]).start()
self.wrapper = BGWidget()
wrapperlayout = QHBoxLayout()
wrapperlayout.setContentsMargins(0,0,0,0)
wrapperlayout.setSpacing(0)
self.wrapper.setLayout(wrapperlayout)
self.header = QFrame()
headerlayout = QHBoxLayout()
headernest = QVBoxLayout()
headernestwidget = QWidget()
headernestwidget.setContentsMargins(0,0,0,0)
self.header.setContentsMargins(0,0,0,0)
wrapperlayout.addWidget(self.header)
self.header.setLayout(headerlayout)
headernestwidget.setLayout(headernest)
uDN = EmojiText(chat['account']['display_name'], chat['account']['emojis'], header=True)
if self.acct.find('@') == -1:
uACT = LinkLabel(chat['account']['url'], '@'+self.acct+'@'+ex.getCurrentAcc()[1])
else:
uACT = LinkLabel(chat['account']['url'], '@'+self.acct)
uACT.setMargin(0)
#uACT.setStyleSheet("color: white;")
headernest.addWidget(uDN)
headernest.addWidget(uACT)
self.avatar = QLabel()
self.avatar.setMargin(0)
self.avatar.setFixedHeight(round(QDesktopWidget().screenGeometry(-1).height() * 0.06))
headerlayout.addWidget(self.avatar)
headerlayout.addWidget(headernestwidget, 100)
self.messageArea = MessageArea(self.chatID, self.account, self.callMe)
self.msgs = MovingScroll(self.messageArea)
if ex.fetchBackgroundsAction.isChecked():
if self.acct.find('@') == -1:
remoteURL = ex.getCurrentAcc()[1]
else:
remoteURL = self.acct.split('@')[1]
self.msgs.remoteURL = remoteURL
CallThread(requests.get, self.msgs.parseNodeInfo, 'https://'+remoteURL+'/api/v1/instance').start()
del remoteURL
self.msgs.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.msgs.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
self.msgs.setWidgetResizable(True)
self.messageArea.setFixedWidth(self.msgs.getChildWidth())
self.messageArea.update()
send = SendArea(self.chatID)
layout = QVBoxLayout()
layout.addWidget(self.wrapper, 2)
layout.addWidget(self.msgs, 35)
layout.addWidget(send, 1)
self.setFocusProxy(send.getTextbox())
self.setLayout(layout)
def callMe(self):
self.msgs.setWidget(self.messageArea)
def markRead(self):
self.messageArea.markRead()
def headerReady(self, path):
self.wrapper.setPath(path)
def addMessage(self, message):
self.messageArea.messages.insert(0, message)
self.messageArea._update(self.messageArea.messages)
def useAvi(self, path):
pic = QPixmap(path)
ava = QPixmap(path)
if pic.isNull():
pic = QPixmap(APPDATA+'/'+ex.getCurrentAcc()[1]+'avi.png')
ava = QPixmap(APPDATA+'/'+ex.getCurrentAcc()[1]+'avi.png')
icon = QIcon()
icon.addPixmap(pic)
self.parent.setTabIcon(self.parent.indexOf(self), icon)
ava = ava.scaledToHeight(round(QDesktopWidget().screenGeometry(-1).height() * 0.06), mode=Qt.SmoothTransformation)
self.avatar.resize(ava.width(), ava.height())
self.avatar.setPixmap(ava)
self.avatar.setFixedWidth(self.avatar.width())
def paintEvent(self, event):
super().paintEvent(event)
if not self.wrapper.pixmap.isNull():
self.header.setStyleSheet(".QFrame{background-color: rgba(0, 0, 0, 0.3);} QFrame{color: white;}")
self.paintEvent = super().paintEvent
class MovingScroll(QScrollArea):
def __init__(self, child, path=None):
super().__init__()
self.child = child
self.remoteURL = ''
self.verticalScrollBar().rangeChanged.connect(self.scrollToBottom)
self.verticalScrollBar().valueChanged.connect(self.handleValue)
def resizeEvent(self, event):
self.child.setFixedWidth(self.getChildWidth())
def scrollToBottom(self):
self.verticalScrollBar().triggerAction(QAbstractSlider.SliderToMaximum)
def getChildWidth(self):
return self.width()-self.verticalScrollBar().width()-5
def handleValue(self, pos):
if pos < (self.verticalScrollBar().maximum() / 10):
self.child.addPage()
def parseNodeInfo(self, response):
try:
CallThread(getPic, self.child.setPath, 'https://'+self.remoteURL+response.json()['background_image']).start()
except:
print("Couldn't download background image for: "+remoteURL)
class MessageArea(QWidget):
def __init__(self, chatID, account, callback):
super().__init__()
self.chatID = chatID
self.account = account
self.callback = callback
self.path = None
self.pixmap = QPixmap()
self.layout = QVBoxLayout()
self.layout.setContentsMargins(10, 0,-5,0)
self.last_read_id = None
self.messages = []
self.page = 0
self.fetchingPage = False
def update(self):
u, i = ex.getCurrentAcc()
CallThread(ex.accts[u+i].getMessages, self._update, self.chatID).start()
def markRead(self):
u, i = ex.getCurrentAcc()
if not self.last_read_id or not u or not i:
return
acc = ex.accts[u+i]
CallThread(acc.markChatRead, None, self.chatID, self.last_read_id).start()
def addPage(self):
if self.fetchingPage:
return
self.fetchingPage = True
u, i = ex.getCurrentAcc()
self.page += 1
CallThread(ex.accts[u+i].getMessages, self.pageReady, self.chatID, past=self.messages[len(self.messages)-1]['id']).start()
def pageReady(self, messages):
for message in messages:
self.messages.append(message)
self._update(self.messages)
def _update(self, messages):
self.messages = messages
for message in messages:
if message['account_id'] == self.account['id']:
self.last_read_id = message['id']
break
for i in reversed(range(self.layout.count())):
self.layout.itemAt(i).widget().setParent(None)
for message in reversed(messages):
#for line in re.split(newlineRegex, message['content']):
# if line.strip() == '':
# continue
self.layout.addWidget(SingleMessage(message, self.account, self), 0, Qt.AlignBottom)
self.setLayout(self.layout)
self.callback()
if self.fetchingPage:
self.fetchingPage = False
def getMessages(self):
return self.messages or []
def setPath(self, path):
self.path = path
def paintEvent(self, event):
super().paintEvent(event)
if self.pixmap.isNull() and self.path:
self.pixmap = QPixmap(self.path)
painter = QPainter(self)
if not self.pixmap.isNull():
self.pixmap = self.pixmap.scaled(self.width(), self.height(), Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation)
painter.drawPixmap(0,0, self.pixmap)
newlineRegex = re.compile('<\/?br\/?>|\n')
class SingleMessage(QWidget):
def __init__(self, message, account, parent):
super().__init__(parent=parent)
self.parent = parent
self.message = message
self.account = account
self.layout = QHBoxLayout()
self.layout.setContentsMargins(0,0,0,0)
self.layout.setSpacing(0)
self.setStyleSheet("MessageAvatar{margin: 0em 0.2em 0em 0.2em;border: 1px solid black;}")
if ex.animatePicturesAction.isChecked():
avatar_prop = 'avatar'
header_prop = 'header'
else:
avatar_prop = 'avatar_static'
header_prop = 'header_static'
self.avaURL = self.account[avatar_prop]
u,i = ex.getCurrentAcc()
self.userPixmap = None
self.convoPixmap = None
CallThread(getPic, self.setUserPixmap, ex.accts[u+i].acct[avatar_prop]).start()
CallThread(getPic, self.setConvoPixmap, self.avaURL).start()
if self.message['account_id'] == self.account['id']:
self.layout.addWidget(MessageAvatar('convo', self), 0, (Qt.AlignLeft | Qt.AlignTop))
self.layout.addWidget(InternalMessage(self.message, self.account, self), 1, Qt.AlignLeft)
#spacer = QWidget()
#spacer.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
#self.layout.addWidget(spacer, 9000)
else:
#spacer = QWidget()
#spacer.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
#self.layout.addWidget(spacer, 9000)
self.layout.addWidget(InternalMessage(self.message, self.account, self), 1, Qt.AlignRight)
self.layout.addWidget(MessageAvatar('user', self), 0, (Qt.AlignRight | Qt.AlignTop))
self.setLayout(self.layout)
def setUserPixmap(self, path):
p = QPixmap(path)
if p.isNull():
p = QPixmap(APPDATA+'/'+ex.getCurrentAcc()[1]+'avi.png')
p = p.scaledToHeight(round(QDesktopWidget().screenGeometry(-1).height() / 21.6), mode=Qt.SmoothTransformation)
self.userPixmap = p
def setConvoPixmap(self, path):
p = QPixmap(path)
if p.isNull():
p = QPixmap(APPDATA+'/'+ex.getCurrentAcc()[1]+'avi.png')
p = p.scaledToHeight(round(QDesktopWidget().screenGeometry(-1).height() / 21.6), mode=Qt.SmoothTransformation)
self.convoPixmap = p
class MessageAvatar(QLabel):
def __init__(self, futurepixmap, parent):
super().__init__(parent=parent)
self.parent = parent
self.pixmap = QPixmap()
self.setMargin(0)
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
CallThread(self.awaitPixmap, None, futurepixmap).start()
#self.setPixmap(self.pixmap)
def setPath(self, path):
#if not self.pixmap.isNull():
# return
#self.pixmap = QPixmap(path)
#if self.pixmap.isNull():
# self.pixmap = QPixmap(APPDATA+'/'+ex.getCurrentAcc()[1]+'avi.png')
#self.pixmap = self.pixmap.scaledToHeight(50, mode=Qt.SmoothTransformation)
self.pixmap = path
self.resize(self.pixmap.width(), self.pixmap.height())
self.setPixmap(self.pixmap)
self.setFixedWidth(self.pixmap.width())
def awaitPixmap(self, p):
if p == 'user':
while not self.parent.userPixmap:
monkeypatch.sleep(0.2)
self.resize(self.parent.userPixmap.width(), self.parent.userPixmap.height())
self.setPixmap(self.parent.userPixmap)
self.setFixedWidth(self.parent.userPixmap.width())
else:
while not self.parent.convoPixmap:
monkeypatch.sleep(0.2)
self.resize(self.parent.convoPixmap.width(), self.parent.convoPixmap.height())
self.setPixmap(self.parent.convoPixmap)
self.setFixedWidth(self.parent.convoPixmap.width())
class InternalMessage(QFrame):
def __init__(self, message, account, parent):
super().__init__(parent=parent)
self.parent = parent
self.message = message
self.account = account
self.layout = QHBoxLayout()
self.layout.setContentsMargins(0,0,0,0)
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
self.attachment = None
if ex.darkModeAction.isChecked():
color_text="#ffffff"
color_bg = "#000000"
else:
color_text="#000000"
color_bg = "#ffffff"
self.setStyleSheet("InternalMessage{background-color: "+color_bg+";border-radius: 1em;padding: 0.5em;border: 1px solid "+color_text+";} QWidget{color:"+color_text+";}")
if self.message['content']:
self.label = EmojiText(self.message['content'], self.message['emojis'])
self.label.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
else:
self.label = None
self.timestamp = QLabel(timeconvert.utc_to_local(self.message['created_at'], ex.twoFourTimeAction.isChecked()))
self.timestamp.setFont(ex.TimestampFont)
self.timestamp.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
if self.label:
self.layout.addWidget(self.label)
if not self.message['attachment']:
self.layout.addWidget(self.timestamp, 1, (Qt.AlignRight | Qt.AlignBottom))
else:
CallThread(self.getMime, self.addAttachment, self.message['attachment']['url']).start()
#self.setMinimumSize(QSize(50, computedHeight))
self.setLayout(self.layout)
def getMime(self, url):
with urllib.request.urlopen(url) as res:
# pleroma sux balls at mime types
mime = magic.from_buffer(res.read(2048), mime=True)
return mime
def addAttachment(self, mime):
self.attachment = MediaAttachment(self.message['attachment'], mime)
self.layout.addWidget(self.attachment)
self.layout.addWidget(self.timestamp, 1, (Qt.AlignRight | Qt.AlignBottom))
def sizeHint(self):
multiplier = 2
additive = 0
if self.label:
multiplier += self.label.layoutfinal.count()
if self.attachment:
additive += self.attachment.getMedia().height()
computedHeight = (ex.defaultFontMetrics.height() * multiplier) + additive
computedWidth = 50
if self.attachment:
if self.attachment.getMedia().width() > computedWidth:
computedWidth = self.attachment.getMedia().width()
return QSize(computedWidth, computedHeight)
def MediaAttachment(media, mime):
if mime.find("image/") != -1:
return ImageAttachment(media)
elif mime.find("video/") != -1:
return VideoAttachment(media)
elif mime.find("audio/") != -1:
return AudioAttachmentWrapper(media)
else:
return FileAttachment(media)
class FileAttachment(QPushButton):
def __init__(self, media):
super(FileAttachment, self).__init__()
self.url = QUrl(media['url'])
self.setCursor(Qt.PointingHandCursor)
self.setIcon(self.style().standardIcon(QStyle.SP_FileLinkIcon))
self.setFlat(True)
def mouseReleaseEvent(self, event):
if event.modifiers() == Qt.NoModifier and event.button() == Qt.LeftButton:
QDesktopServices.openUrl(self.url)
def getMedia(self):
return self
class AudioAttachmentWrapper(QWidget):
def __init__(self, media):
super(AudioAttachmentWrapper, self).__init__()
layout = QHBoxLayout()
self.file = FileAttachment(media)
self.audio = AudioAttachment(media)
layout.addWidget(self.file)
layout.addWidget(self.audio)
self.setLayout(layout)
def getMedia(self):
return QSize(self.file.getMedia().width()+self.audio.getMedia().width(), self.file.getMedia().height()+self.audio.getMedia().height())
class AudioAttachment(audiowidget.AudioPlayer):
def __init__(self, media):
super(AudioAttachment, self).__init__()
#CallThread(self.openFile, None, media['url']).start()
self.openFile(media['url'])
def getMedia(self):
return super().sizeHint()
class LinkLabel(QLabel):
def __init__(self, url, *args, **kwargs):
super(LinkLabel, self).__init__(*args, **kwargs)
self.url = QUrl(url)
self.setCursor(Qt.PointingHandCursor)
def mouseReleaseEvent(self, event):
bttn = event.button()
modif = event.modifiers()
if modif == Qt.NoModifier and bttn == Qt.LeftButton:
QDesktopServices.openUrl(self.url)
class VideoAttachment(videowidget.VideoPlayer):
def __init__(self, media, *args, **kwargs):
super().__init__(media['url'], *args, **kwargs)
#CallThread(self.openFile, None, media['url']).start()
self.openFile(media['url'])
def getMedia(self):
return QSize(self.width(), self.videoWidget.height()+50)
class ImageAttachment(LinkLabel):
def __init__(self, media):
super().__init__(media['remote_url'])
self.pixmap = QPixmap()
CallThread(getPic, self.setPath, media['url']).start()
def setPath(self, path):
self.pixmap = QPixmap(path)
if round(ex.width() * 0.7) < self.pixmap.width():
self.pixmap = self.pixmap.scaledToWidth(round(ex.width() * 0.7), mode=Qt.SmoothTransformation)
self.resize(self.pixmap.width(), self.pixmap.height())
self.setPixmap(self.pixmap)
#self.setFixedHeight(self.height())
self.url = QUrl(path)
def getMedia(self):
return self.pixmap
class SendArea(QWidget):
def __init__(self, chatID):
super().__init__()
self.layout = QHBoxLayout()
self.lastFile = None
self.mediaPreview = None
self.mediaID = None
self.Err = QErrorMessage()
self.chatID = chatID
self.textbox = TextBox()
self.mediaStack = QStackedWidget()
self.attachButton = QPushButton(self.style().standardIcon(QStyle.SP_FileDialogStart), 'Attach File')
self.attachButton.clicked.connect(self.newFileDialog)
self.attachButton.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)
sendButton = QPushButton(QIcon(QPixmap(os.path.join(os.path.dirname(__file__), "send.svg"))), '')
sendButton.clicked.connect(self.sendMessage)
sendButton.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)
self.mediaStack.addWidget(self.attachButton)
self.mediaStack.setCurrentWidget(self.attachButton)
self.layout.addWidget(self.textbox, 8)
self.layout.addWidget(self.mediaStack, 1)
self.layout.addWidget(sendButton, 1)
self.setFocusProxy(self.textbox)
self.setLayout(self.layout)
self.textbox.setSendFunction(self.sendMessage)
def getTextbox(self):
return self.textbox
def newFileDialog(self):
if self.mediaPreview == 'uploading':
self.Err.showMessage("Already uploading a file!")
return
qfd = QFileDialog(self)
qfd.setFileMode(QFileDialog.ExistingFile)
qfd.setNameFilters(["All Files (*)"])
qfd.filesSelected.connect(self.attachFile)
qfd.show()
def attachFile(self, paths):
if paths[0] == self.lastFile:
return
elif self.mediaPreview:
self.Err.showMessage("File already attached!")
return
u, i = ex.getCurrentAcc()
CallThread(ex.accts[u+i].uploadMedia, self.doneAttachFile, paths[0]).start()
self.mediaPreview = 'uploading'
self.lastFile = paths[0]
def doneAttachFile(self, media):
if not 'id' in media:
self.Err.showMessage('Error uploading file')
self.detachFile()
return
self.mediaID = media['id']
self.mediaPreview = QPushButton(ex.style().standardIcon(QStyle.SP_FileIcon), '')
self.mediaPreview.clicked.connect(self.detachDialog)
self.mediaPreview.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)
self.mediaStack.addWidget(self.mediaPreview)
self.mediaStack.setCurrentWidget(self.mediaPreview)
def detachDialog(self):
dialog = DetachDialog()
dialog.accepted.connect(self.detachFile)
dialog.getInput()
def detachFile(self):
if self.mediaPreview and type(self.mediaPreview) != str:
self.mediaStack.removeWidget(self.mediaPreview)
self.mediaStack.setCurrentWidget(self.attachButton)
self.mediaPreview = None
self.lastFile = None
self.mediaID = None
def sendMessage(self):
u, i = ex.getCurrentAcc()
acc = ex.accts[u+i]
if self.textbox.toPlainText() == '':
text = None
else:
text = self.textbox.toPlainText()
CallThread(acc.sendMessage, None, self.chatID, content=text, media=self.mediaID).start()
self.textbox.setText('')
if self.mediaPreview:
self.detachFile()
self.textbox.setFocus(Qt.NoFocusReason)
class TextBox(QTextEdit):
def __init__(self, sendFunction=None):
super().__init__()
self.sendFunction = sendFunction or self.noop
def keyPressEvent(self, event):
if not event.matches(QKeySequence.InsertParagraphSeparator):
super().keyPressEvent(event)
else:
self.sendFunction()
def setSendFunction(self, sendFunction):
self.sendFunction = sendFunction
def noop(self):
return
class DetachDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setWindowTitle("Detach Dialog")
QBtn = QDialogButtonBox.Ok | QDialogButtonBox.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.layout = QVBoxLayout()
self.message = QLabel("Detach this file?")
self.layout.addWidget(self.message)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
def getInput(self):
self.exec_()
self.show()
self.raise_()
self.activateWindow()
class TotpCard(QDialog):
def __init__(self, acct, challenge_types, parent=None,):
super().__init__(parent=parent)
self.setWindowTitle("MFA Request")
self.result = None
self.waiting = False
QBtn = QDialogButtonBox.Ok
layout = QVBoxLayout()
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.message = QLabel("2FA Required for "+acct)
tmp = QHBoxLayout()
tmpw = QWidget()
tmpw.setLayout(tmp)
self.comboboxlabel = QLabel("2FA Code Type:")
self.combobox = QComboBox()
self.combobox.addItems(challenge_types)
self.combobox.setDuplicatesEnabled(False)
self.combobox.setEditable(False)
tmp.addWidget(self.comboboxlabel)
tmp.addWidget(self.combobox)
self.code = QLineEdit()
self.code.setPlaceholderText('2FA Code')
layout.addWidget(self.message)
layout.addWidget(tmpw)
layout.addWidget(self.code)
layout.addWidget(self.buttonBox)
self.setLayout(layout)
self.setFocusProxy(self.code)
self.accepted.connect(self._finished)
def _finished(self):
ex.totpReady = (self.code.text(), self.combobox.currentText())
def getInput(self, *args, **kwargs):
self.exec_()
self.show()
self.raise_()
self.activateWindow()
class LoginDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setWindowTitle("Log In")
QBtn = QDialogButtonBox.Ok | QDialogButtonBox.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.layout = QVBoxLayout()
self.message = QLabel("Log in to your fediverse account")
self.username = QLineEdit()
self.username.setPlaceholderText('Username (e.g. Moon)')
self.instance = QLineEdit()
self.instance.setPlaceholderText('Instance (e.g. shitposter.club)')
self.password = QLineEdit()
self.password.setPlaceholderText('Password')
self.password.setEchoMode(QLineEdit.Password)
self.layout.addWidget(self.message)
self.layout.addWidget(self.username)
self.layout.addWidget(self.instance)
self.layout.addWidget(self.password)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
self.setFocusProxy(self.username)
def getInput(self, func):
self.show()
self.raise_()
self.activateWindow()
self.accepted.connect(self._finished)
self.finishedFunc = func
def _finished(self):
instance = self.instance.text()
instance = instance.replace('http://', '')
instance = instance.replace('https://', '')
if instance[len(instance) - 1] == '/':
instance = instance[0:(len(instance) - 2)]
self.finishedFunc(self.instance.text(), self.username.text(), password=self.password.text())
class ContactCard(QDialog):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setWindowTitle("Contact Info")
QBtn = QDialogButtonBox.Ok
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.layout = QVBoxLayout()
self.message = QLabel("Written by knotteye\nContact me in the following places (in order of preference) with issues")
self.fedi = QLabel("Fedi: <a href='https://waldn.net/users/knotteye'>@knotteye@waldn.net</a>")
self.fedi.setOpenExternalLinks(True)
self.xmpp = QLabel('XMPP: <a href="xmpp://knotteye@telekem.net?message">knotteye@telekem.net</a>')
self.xmpp.setOpenExternalLinks(True)
self.email = QLabel('Email: <a href="mailto:knotteye@airmail.cc">knotteye@airmail.cc</a>')
self.email.setOpenExternalLinks(True)
self.layout.addWidget(self.message)
self.layout.addWidget(self.fedi)
self.layout.addWidget(self.xmpp)
self.layout.addWidget(self.email)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
self.show()
self.raise_()
self.activateWindow()
class LicenseArea(QScrollArea):
def __init__(self, licenseText):
super().__init__()
label = QLabel(licenseText)
self.setWidget(label)
class LicenseCard(QDialog):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setWindowTitle("Required Licensing Information")
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
QBtn = QDialogButtonBox.Ok
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.layout = QVBoxLayout()
self.tabs = QTabWidget()
plchatlayout = QVBoxLayout()
plchatW = QWidget()
plchatW.setLayout(plchatlayout)
plchatlabel = QLabel("<a href='https://git.waldn.net/git/knotteye/plchat/'>Repository</a>")
plchatlabel.setTextFormat(Qt.RichText)
plchatlabel.setOpenExternalLinks(True)
plchatLicense = QLabel(misc.plchatLicense)
plchatscroll = QScrollArea()
plchatscroll.setWidget(plchatLicense)
plchatlayout.addWidget(plchatlabel)
plchatlayout.addWidget(plchatscroll)
self.tabs.addTab(plchatW, 'PlChat')
self.tabs.addTab(LicenseArea(misc.pyqt5License), "PyQt5")
self.tabs.addTab(LicenseArea(misc.videowidgetlicense), "videowidget.py")
self.tabs.addTab(LicenseArea(misc.videowidgetlicense), "audiowidget.py")
self.tabs.addTab(LicenseArea(misc.notifypylicense), "notify-py")
self.tabs.addTab(LicenseArea(misc.logurulicense), "loguru")
self.tabs.addTab(LicenseArea(misc.websocketslicense), "websockets")
self.tabs.addTab(LicenseArea(misc.sixlicense), "six")
self.tabs.addTab(LicenseArea(misc.magiclicense), "python-magic")
self.tabs.addTab(LicenseArea(misc.dateutillicense), "dateutil")
self.tabs.addTab(LicenseArea(misc.keyringlicense), "keyring")
self.layout.addWidget(self.tabs)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
self.show()
self.raise_()
self.activateWindow()
def sizeHint(self):
return QSize(650, 850)
class EmojiText(QWidget):
ceRegex = re.compile('(:[^:]+:)')
def __init__(self, text, emojiList, header=False):
super().__init__()
self.layoutfinal = QVBoxLayout()
self.layoutfinal.setSpacing(0)
self.layoutfinal.setContentsMargins(0,0,0,0)
for line in re.split(newlineRegex, text):
self.layoutfinal.addWidget(self.makeLine(line, emojiList, header))
self.setLayout(self.layoutfinal)
def makeLine(self, text, emojiList, header):
widget = QWidget()
layout = QWidget()
layout = QHBoxLayout()
layout.setSpacing(0)
layout.setContentsMargins(0,0,0,0)
widget.setContentsMargins(0,0,0,0)
customEmojis = []
if header:
size = ex.headerEmojiFontSize
else:
size = ex.emojiFontSize
for em in re.findall(self.ceRegex, text):
for emoji in emojiList:
if emoji['shortcode'] == em[1:len(em)-1]:
cemurl = emoji[STATIC_PREF+'url']
customEmojis.append(CustomEmoji(em[1:len(em)-1], cemurl, size))
i = 0
for stringpart in re.split(self.ceRegex, text):
if len(stringpart) > 2 and stringpart[0] == ':' and stringpart[len(stringpart)-1] == ':':
layout.addWidget(customEmojis[i], 1)
i += 1
else:
label = QLabel(stringpart)
if header:
label.setFont(ex.HeaderFont)
label.setTextFormat(Qt.RichText)
label.setWordWrap(True)
label.setTextInteractionFlags((Qt.TextBrowserInteraction))
label.setOpenExternalLinks(True)
layout.addWidget(label)
layout.addWidget(QWidget(), 9000)
widget.setLayout(layout)
return widget
class CustomEmoji(QLabel):
def __init__(self, shortcode, url=None, height=12):
super().__init__()
self.setScaledContents=True
if url:
CallThread(getPic, self.loadPic, url).start()
self.mheight = height
self.pixmap = None
self.setMargin(0)
def loadPic(self, path):
self.pixmap = QPixmap(path)
self.setPixmap(self.pixmap.scaledToHeight(self.mheight, mode=Qt.SmoothTransformation))
class CallThread(threading.Thread):
# Generic thread object that takes some function, a callback and a list of arguments
# The entire purpose of this object is to be a wrapper around running functions in a new thread
def __init__(self, function, callback, *args, **kwargs):
super(CallThread, self).__init__()
self.function = function
self.args = args or ()
self.kwargs = kwargs or {}
self.callback = callback
def run(self):
result = self.function(*self.args, **self.kwargs)
if not self.callback:
return
ex._eventloop.call_soon_threadsafe(self.callback, result)
class RegisterThread(threading.Thread):
def __init__(self, acct, callback):
super(RegisterThread, self).__init__()
self.acct = acct
self.callback = callback
def run(self):
self.acct.register()
try:
self.acct.login()
except ValueError:
ex._eventloop.call_soon_threadsafe(ex.badLogin, self.acct.username+'@'+self.acct.instance)
return
NotifThread(self.acct).start()
self.acct.setChatUpdate(ex.handlePleromaEvent)
self.callback(self.acct)
class NotifThread(threading.Thread):
def __init__(self, acct):
super(NotifThread, self).__init__()
self.acct = acct
self.loop = asyncio.new_event_loop()
self.daemon = True
asyncio.set_event_loop(self.loop)
def run(self):
self.loop.run_until_complete(self.acct.startStreaming())
class HLine(QFrame):
def __init__(self):
super(HLine, self).__init__()
self.setFrameShape(self.HLine|self.Sunken)
class VLine(QFrame):
def __init__(self):
super(VLine, self).__init__()
self.setFrameShape(self.VLine|self.Sunken)
class IconLabel(QLabel):
def __init__(self, icon, w=30, h=None):
super(IconLabel, self).__init__()
if not h:
h = w
#self.icon = self.style().standardIcon(icon)
self.icon = icon
self.setPixmap(self.icon.pixmap(QSize(w, h)))
Notification = Notify(
default_notification_title="PlChat",
default_notification_icon=ICON_PATH,
default_notification_audio=NOTIF_SOUND
)
def sendNotification(message, title=None, icon=None):
Notification.message = message
if title:
Notification.title = title
if icon:
Notification.icon = icon
Notification.send(block=False)
def getPic(url):
filename = urllib.parse.urlparse(url).netloc+os.path.basename(urllib.parse.urlparse(url).path)
filepath = CACHE+'/img/'+filename
try:
f = open(filepath, mode='xb+')
THREADS[filepath] = threading.current_thread()
except FileExistsError:
try:
while THREADS[filepath].is_alive():
monkeypatch.sleep(0.2)
return filepath
except KeyError:
return filepath
try:
imgdata = urllib.request.urlopen(url).read()
except Exception as E:
print("Got "+str(E)+" while downloading file: "+url)
return filepath
f.write(imgdata)
f.close()
return filepath
def getVid(url):
filename = urllib.parse.urlparse(url).netloc+os.path.basename(urllib.parse.urlparse(url).path)
filepath = CACHE+'/vid/'+filename
try:
f = open(filepath, mode='xb+')
THREADS[filepath] = threading.current_thread()
except FileExistsError:
try:
while THREADS[filepath].is_alive():
monkeypatch.sleep(0.2)
return filepath
except KeyError:
return filepath
try:
viddata = urllib.request.urlopen(url).read()
except Exception as E:
print("Got "+str(E)+" while downloading file: "+url)
return filepath
f.write(viddata)
f.close()
return filepath
def getAudio(url):
filename = urllib.parse.urlparse(url).netloc+os.path.basename(urllib.parse.urlparse(url).path)
filepath = CACHE+'/audio/'+filename
try:
f = open(filepath, mode='xb+')
THREADS[filepath] = threading.current_thread()
except FileExistsError:
try:
while THREADS[filepath].is_alive():
monkeypatch.sleep(0.2)
return filepath
except KeyError:
return filepath
try:
viddata = urllib.request.urlopen(url).read()
except Exception as E:
print("Got "+str(E)+" while downloading file: "+url)
return filepath
f.write(viddata)
f.close()
return filepath
def _mkdir(_dir):
if os.path.isdir(_dir): pass
elif os.path.isfile(_dir):
raise OSError("%s exists as a regular file." % _dir)
else:
parent, directory = os.path.split(_dir)
if parent and not os.path.isdir(parent): _mkdir(parent)
if directory: os.mkdir(_dir)
def getAvi(instance):
filename = instance+'avi.png'
filepath = APPDATA+'/'+filename
try:
f = open(filepath, mode='xb+')
except FileExistsError:
return
try:
imgdata = urllib.request.urlopen('https://'+instance+'/images/avi.png').read()
except Exception as E:
print("Couldn't get default avi for "+instance+", error: "+str(E))
return
f.write(imgdata)
f.close()
return filepath
def updateAnimationPref():
if ex.animatePicturesAction.isChecked():
STATIC_PREF = ''
else:
STATIC_PREF = 'static_'
if __name__ == '__main__':
_mkdir(CACHE+'/img/')
_mkdir(CACHE+'/vid/')
_mkdir(CACHE+'/audio/')
_mkdir(APPDATA)
app = QApplication(sys.argv)
app.setOrganizationName("plchat")
app.setApplicationName("plchat")
ex = App(app.processEvents)
sys.exit(ex._eventloop.run_forever())