1586 lines
60 KiB
Python
1586 lines
60 KiB
Python
#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()) |