init
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
[about]
|
||||
thanks = Thanks
|
||||
developers = Developers
|
||||
translators = Translators
|
||||
translators_text = "Thanks to all the translators"
|
||||
software = Software
|
||||
resources = Resources
|
||||
api = API
|
||||
info = Movim is an XMPP-based communication platform. All the project, except the following software and resources, is under
|
||||
sourcecode_title = Sourcecode
|
||||
sourcecode_text = Check out the sourcecode on the Github repository
|
||||
@@ -0,0 +1,71 @@
|
||||
var Account = {
|
||||
resetPassword: function () {
|
||||
var form = document.querySelector('form[name=password]');
|
||||
form.reset();
|
||||
document.querySelector('#password_save').className = 'button color flat';
|
||||
},
|
||||
resolveSessionsStates: function () {
|
||||
var store = new ChatOmemoStorage();
|
||||
store.getSessionsIds(store.jid).map(id => {
|
||||
store.getSessionState(store.jid + '.' + id).then(state => {
|
||||
if (state) {
|
||||
let checkbox = document.querySelector('input[name=accountsessionstate_' + id + ']');
|
||||
|
||||
if (checkbox) {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
},
|
||||
toggleFingerprintState: function (checkbox) {
|
||||
var store = new ChatOmemoStorage();
|
||||
store.setSessionState(checkbox.dataset.identifier, checkbox.checked);
|
||||
},
|
||||
deleteBundle: async function (bundleid) {
|
||||
ChatOmemo.resolveContactFingerprints(USER_JID).then(remoteKeys => {
|
||||
remoteKeys = remoteKeys.filter(remoteKey =>
|
||||
remoteKey.bundleid == bundleid
|
||||
);
|
||||
|
||||
Account_ajaxDeleteBundle(remoteKeys[0])
|
||||
});
|
||||
},
|
||||
deleteBundleConfirm: function (bundleid) {
|
||||
var store = new ChatOmemoStorage();
|
||||
|
||||
var address = new libsignal.SignalProtocolAddress(USER_JID, bundleid);
|
||||
store.removeSession(address);
|
||||
|
||||
Account_ajaxDeleteBundleConfirm(bundleid, store.getOwnSessionsIds());
|
||||
},
|
||||
refreshFingerprints: async function () {
|
||||
ChatOmemo.resolveContactFingerprints(USER_JID).then(remoteKeys => {
|
||||
let omemoStorage = new ChatOmemoStorage;
|
||||
|
||||
omemoStorage.getLocalRegistrationId().then(localBundleId => {
|
||||
omemoStorage.getIdentityKeyPair().then(identityKey => {
|
||||
// Remove it if its already there
|
||||
remoteKeys = remoteKeys.filter(remoteKey =>
|
||||
remoteKey.bundleid != localBundleId.toString()
|
||||
);
|
||||
|
||||
remoteKeys.unshift({
|
||||
jid: USER_JID,
|
||||
self: true,
|
||||
bundleid: localBundleId.toString(),
|
||||
fingerprint: MovimUtils.arrayBufferToBase64(identityKey.pubKey)
|
||||
});
|
||||
|
||||
Account_ajaxHttpGetFingerprints(remoteKeys);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MovimWebsocket.attach(function () {
|
||||
if (OMEMO_ENABLED) Account.refreshFingerprints();
|
||||
Account_ajaxHttpGetPresences();
|
||||
Account_ajaxGetGateways();
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
[account]
|
||||
title = Account
|
||||
account_management = Account management
|
||||
password_change_title = Change my password
|
||||
password = Password
|
||||
password_confirmation = Password confirmation
|
||||
password_changed = The password has been updated
|
||||
password_not_same = The provided passwords are not the same
|
||||
delete = Delete your account
|
||||
delete_text = You will delete your XMPP account and all the relative information linked to it (profile, contacts, and publications).
|
||||
delete_text_confirm = Type your full username
|
||||
delete_text_error = Please provide a valid username.
|
||||
gateway_title = Gateways
|
||||
clear = Leave this instance
|
||||
clear_text = You will delete all the relative information linked to your account on this Movim instance (profile, contacts, and publications).
|
||||
clear_text_confirm = This action will not delete your account but will disconnect you from Movim.
|
||||
delete_bundle_title = OMEMO Key deletion
|
||||
delete_bundle_text = You are going to destroy the following OMEMO key. Your contacts will not be able to encrypt messages using that key anymore. This action cannot be reverted. Are you sure?
|
||||
xmpp_server_info = XMPP Server Information
|
||||
@@ -0,0 +1,54 @@
|
||||
var AdHoc = {
|
||||
refresh: function () {
|
||||
var items = document.querySelectorAll('.adhoc_widget .actions li:not(.subheader)');
|
||||
var i = 0;
|
||||
|
||||
while (i < items.length) {
|
||||
items[i].onclick = function () {
|
||||
Drawer.clear();
|
||||
AdHoc_ajaxCommand(this.dataset.jid, this.dataset.node);
|
||||
};
|
||||
|
||||
i++;
|
||||
}
|
||||
},
|
||||
initForm: function () {
|
||||
var form = document.querySelector('#dialog form[name=command]');
|
||||
|
||||
if (form) {
|
||||
form.addEventListener('input', e => AdHoc.checkFormValidity());
|
||||
}
|
||||
|
||||
AdHoc.checkFormValidity();
|
||||
},
|
||||
submit: function (jid, action) {
|
||||
var form = document.querySelector('#dialog form[name=command]');
|
||||
AdHoc_ajaxSubmit(
|
||||
jid,
|
||||
form.dataset.node,
|
||||
MovimUtils.formToJson('command'),
|
||||
form.dataset.sessionid,
|
||||
action
|
||||
);
|
||||
},
|
||||
checkFormValidity: function () {
|
||||
var form = document.querySelector('#dialog form[name=command]');
|
||||
var action = document.querySelector('#dialog #adhoc_action');
|
||||
|
||||
if (form && action) {
|
||||
if (form.checkValidity()) {
|
||||
action.classList.remove('disabled');
|
||||
} else {
|
||||
action.classList.add('disabled');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MovimWebsocket.attach(function () {
|
||||
var parts = MovimUtils.urlParts();
|
||||
|
||||
if (parts.page === "configuration") {
|
||||
AdHoc_ajaxGet();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
[adhoc]
|
||||
title = Actions
|
||||
completed = Action completed
|
||||
|
||||
[tools]
|
||||
title = Developer Tools
|
||||
sdp_to_jingle = Convert SDP to Jingle
|
||||
sdp_title = SDP
|
||||
jingle_to_sdp = Convert Jingle to SDP
|
||||
jingle_title = Jingle stanza
|
||||
@@ -0,0 +1,72 @@
|
||||
[admin]
|
||||
general = General Settings
|
||||
|
||||
[general]
|
||||
theme = Theme
|
||||
language = Default language
|
||||
log_verbosity = Log verbosity
|
||||
join_title = Your instance is public?
|
||||
join_text = Register your instance on join.movim.eu and join the Movim federation!
|
||||
|
||||
[restrictsuggestions]
|
||||
title = Restrict suggestions
|
||||
text = "Only suggest chatrooms, Communities, and other contents that are available on the user's XMPP server and related services"
|
||||
|
||||
[chatonly]
|
||||
title = Chat only
|
||||
text = Disable all the social feature (Communities, Blog…) and keep only the chat ones
|
||||
|
||||
[credentials]
|
||||
title = Administration Credential
|
||||
username = Username
|
||||
password = Password
|
||||
re_password = Retype password
|
||||
|
||||
[disableregistration]
|
||||
title = Disable the XMPP registration feature
|
||||
text = Remove the XMPP registration flow and buttons from the interface
|
||||
|
||||
[whitelist]
|
||||
title = Allowed XMPP Servers
|
||||
info1 = If you want to specify a list of authorized XMPP servers on your Movim pod and forbid the connection on all the others please put their domain name(s) here, with comma (ex: movim.eu,jabber.fr)
|
||||
info2 = Leave this field blank if you allow the access to all the XMPP accounts.
|
||||
label = List of allowed XMPP servers
|
||||
|
||||
[ssrfwhitelist]
|
||||
title = Whitelisted Domains for SSRF Protection
|
||||
info1 = Movim is protecting you against SSRF vulnerabilities. This means that Movim blocks all URLs (shared links, avatars, and pictures) that point to your local network IPs.
|
||||
info2 = See the related SSRF documentation on developer.mozilla.org.
|
||||
info3 = Add all domains hosted locally here (for example, if you host a blog, an XMPP file upload service, or other web services).
|
||||
info4 = This setting will be applied after a daemon restart.
|
||||
label = List of Locally Authorized Domains
|
||||
|
||||
[information]
|
||||
description = Description
|
||||
description_placeholder = General description of the instance
|
||||
banner = Main banner
|
||||
banner_info = Ensure that the picture is not too large and can be reached from any webpages
|
||||
info1 = This message will be displayed on the login page. It can be used to link the Terms and Conditions of your Movim instance for example.
|
||||
info2 = Leave this field blank if you don’t want to show any message.
|
||||
label = Information Message
|
||||
|
||||
[xmpp]
|
||||
title = Default XMPP server
|
||||
domain = Main XMPP server domain
|
||||
description = Main XMPP server description
|
||||
country = Main XMPP server country
|
||||
country_pick= Pick a country in the list
|
||||
|
||||
[tenor]
|
||||
title = Tenor integration
|
||||
label = Your Tenor API Key
|
||||
info1 = Movim integrates the Tenor API to allow GIF search and publication in Chat
|
||||
info2 = Access Tenor and get your API key
|
||||
|
||||
[log]
|
||||
empty = Empty
|
||||
syslog = Syslog
|
||||
syslog_files = Syslog and files
|
||||
|
||||
[maxsessions]
|
||||
title = Maximum of connected sessions
|
||||
info = Movim will forbid new users to connect when the limit is reached, set it to 0 to disable
|
||||
@@ -0,0 +1,3 @@
|
||||
#blocked_widget_list:not(:empty) ~ .placeholder {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
MovimWebsocket.attach(function() {
|
||||
AdminReported_ajaxHttpGet();
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
[adminreported]
|
||||
reported_by = "Reported by %s users"
|
||||
info = Those accounts were reported by some Movim users
|
||||
info2 = You can choose to block them for all the other users
|
||||
@@ -0,0 +1,3 @@
|
||||
[adminsessions]
|
||||
title = Sessions
|
||||
text = Currently connected users on the pod
|
||||
@@ -0,0 +1,15 @@
|
||||
#avatar span.primary {
|
||||
min-width: 10rem;
|
||||
height: 10rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
#avatar span.primary i {
|
||||
line-height: 10rem;
|
||||
font-size: 5rem;
|
||||
}
|
||||
|
||||
form[name=bannerform] img.avatar {
|
||||
max-width: calc(100% - 2rem);
|
||||
margin: 1rem;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
MovimWebsocket.attach(function() {
|
||||
Avatar_ajaxHttpGetCurrent();
|
||||
Avatar_ajaxGetAvatar();
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
[avatar]
|
||||
file = File
|
||||
updated = Profile Picture Updated
|
||||
not_updated = Profile Picture Not Updated
|
||||
change = Change my profile picture
|
||||
too_big = Your picture is too big
|
||||
missing = No avatar yet
|
||||
|
||||
[banner]
|
||||
change = Change my banner
|
||||
updated = Banner Updated
|
||||
not_updated = Banner Not Updated
|
||||
missing = No banner yet
|
||||
@@ -0,0 +1,3 @@
|
||||
#blocked_widget_list:not(:empty) ~ .placeholder {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
MovimWebsocket.attach(function() {
|
||||
Blocked_ajaxGet();
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
[blocked]
|
||||
title = Blocked
|
||||
block_account = Block the account
|
||||
unblock_account = Unblock the account
|
||||
account_blocked = Account blocked
|
||||
account_unblocked = Account unblocked
|
||||
placeholder = You will find here the users you have blocked
|
||||
info = The following users are blocked by Movim and cannot contact you anymore
|
||||
info2 = The administrator is also aware of this list and can therefore block those users system wide
|
||||
@@ -0,0 +1,3 @@
|
||||
[blog]
|
||||
title = "%s’s blog"
|
||||
private = This content is private, please login to see it
|
||||
@@ -0,0 +1,110 @@
|
||||
ul#bottomnavigation {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
box-shadow: var(--elevation-6);
|
||||
z-index: 2;
|
||||
display: none;
|
||||
justify-content: center;
|
||||
transition: translate .3s cubic-bezier(.4, 0, .2, 1);
|
||||
user-select: none;
|
||||
background-color: rgb(var(--movim-background));
|
||||
}
|
||||
|
||||
ul#bottomnavigation li {
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
flex: 1 1 0;
|
||||
width: 0;
|
||||
position: relative;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
ul#bottomnavigation li:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
ul#bottomnavigation li:hover:not(.active)>span.primary>i {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
ul#bottomnavigation li:before {
|
||||
content: '';
|
||||
display: block;
|
||||
border-radius: 3rem;
|
||||
height: 5rem;
|
||||
width: 8rem;
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
left: calc(50% - 4rem);
|
||||
}
|
||||
|
||||
ul#bottomnavigation li.active:before {
|
||||
background-color: rgba(var(--movim-element-action), 0.2);
|
||||
}
|
||||
|
||||
ul#bottomnavigation li:not(.active):hover:before {
|
||||
background-color: rgba(var(--movim-element-action), 0.1);
|
||||
}
|
||||
|
||||
ul#bottomnavigation li span:not(.counter) {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1024px) {
|
||||
ul#bottomnavigation {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
ul#bottomnavigation~main>div:not(#chat_widget)>div:last-child,
|
||||
ul#bottomnavigation~main>div:not(#chat_widget)>div.tabelem,
|
||||
ul#bottomnavigation~main>div:not(#chat_widget)>article:last-child {
|
||||
padding-bottom: 6.5rem;
|
||||
}
|
||||
|
||||
body>div.dialog:not(:empty)~ul#bottomnavigation,
|
||||
body>div#drawer:not(.empty)~ul#bottomnavigation,
|
||||
nav.active+ul#bottomnavigation,
|
||||
ul#bottomnavigation.hidden {
|
||||
translate: 0 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
nav.active+ul#bottomnavigation~main,
|
||||
ul#bottomnavigation.hidden~main {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
ul#bottomnavigation~main .button.action {
|
||||
bottom: 8.25rem;
|
||||
transition: all 0.2s ease 0;
|
||||
}
|
||||
|
||||
ul#bottomnavigation~main.enabled .button.action {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
ul#bottomnavigation #bottomnavigation_me span {
|
||||
transform: scale(0.7);
|
||||
}
|
||||
|
||||
ul#bottomnavigation #bottomchatcounter.notifications:after {
|
||||
content: '\e7f4';
|
||||
display: block;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
right: calc(50% - 2.5rem);
|
||||
top: calc(50% + 0.50rem);
|
||||
position: absolute;
|
||||
border-radius: 1rem;
|
||||
background-color: var(--movim-accent);
|
||||
box-shadow: var(--elevation-2);
|
||||
font-family: 'Material Symbols';
|
||||
font-size: 1.2rem;
|
||||
line-height: 2rem;
|
||||
font-variation-settings: 'FILL' 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
var BottomNavigation = {
|
||||
chatsCounter: 0,
|
||||
spacesCounters: {},
|
||||
setChatNotification: function (keys) {
|
||||
let counter = document.querySelector('#bottomchatcounter');
|
||||
|
||||
if (counter) {
|
||||
counter.classList.remove('notifications');
|
||||
|
||||
Object.keys(keys).forEach(key => {
|
||||
if (key == 'chat') {
|
||||
BottomNavigation.chatsCounter = keys[key];
|
||||
}
|
||||
|
||||
if (key.substring(0, 5) == 'space' && !key.includes('|')) {
|
||||
BottomNavigation.spacesCounters[key] = keys[key];
|
||||
}
|
||||
});
|
||||
|
||||
if (BottomNavigation.chatsCounter
|
||||
+ Object.values(BottomNavigation.spacesCounters).reduce((sum, counter) => sum + counter, 0)) {
|
||||
counter.classList.add('notifications');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MovimWebsocket.initiate(() => {
|
||||
if (MovimUtils.isMobile()) {
|
||||
BottomNavigation_ajaxHttpRefresh()
|
||||
}
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,57 @@
|
||||
[message]
|
||||
published = Message Published
|
||||
react = Add a reaction
|
||||
retract = Retract
|
||||
retract_body = "Your contact attempted to retract a previous message, but it's unsupported by your client"
|
||||
moderate_body = This message was moderated
|
||||
error = Your message could not be sent, please try again later
|
||||
encrypted = Encrypted message
|
||||
retracted = Retracted message
|
||||
composing = Composing…
|
||||
invitation = Invitation
|
||||
paused = Paused…
|
||||
gone = Contact gone
|
||||
edit_help = ↑ to edit your previous message
|
||||
emoji_help = :shortcut: to insert an emoji
|
||||
visitor_help = You are not allowed to send messages in this chatroom
|
||||
|
||||
[chat]
|
||||
attention = %s needs your attention
|
||||
placeholder = Your message here…
|
||||
smileys = Smileys
|
||||
frequent = Frequent contacts
|
||||
new_title = Nothing here yet…
|
||||
new_text = "Don’t be shy, send a first message to start the discussion"
|
||||
invitation = You received an invitation to join a chatroom
|
||||
invitation_self = You sent an invitation to join a chatroom
|
||||
invitation_join = Join %s
|
||||
profile = See the profile
|
||||
clear = Clear the history
|
||||
edit_previous = Edit last message
|
||||
report_abuse = Report abuse
|
||||
jingle_incoming = Incoming call
|
||||
jingle_outgoing = Outgoing call
|
||||
jingle_retract = Missed call
|
||||
jingle_reject = Refused call
|
||||
jingle_end = Call ended
|
||||
jingle_hours = %s hours %s minutes
|
||||
jingle_minutes = %s minutes and %s seconds
|
||||
jingle_seconds = %s seconds
|
||||
muc_admin = %s is now admin
|
||||
muc_owner = %s is now owner
|
||||
muc_outcast = %s is now banned
|
||||
muc_member = %s is now member
|
||||
publish_error = An error occurred when sending the message
|
||||
clear_history = Clear the local history
|
||||
clear_history_text = "You are going to delete %s local messages, are you sure?"
|
||||
action_impossible_encrypted = Action impossible in encrypted chats
|
||||
first_messages_title = This is the beginning of this conversation
|
||||
first_messages_text = You can choose to add it as a new contact or block it to prevent some other unsolicited messages
|
||||
first_messages_add = Add to my contacts
|
||||
first_messages_block = Block
|
||||
|
||||
[wide_conference_call]
|
||||
enable_title = Enable Wide Conference Call
|
||||
enable_text = Scale this call to more participants.
|
||||
enable_info = "An SFU (Selective Forwarding Unit) service was detected on this server. Unlike the current peer-to-peer (P2P) setup, where every participant sends their audio/video stream directly to everyone else, an SFU relays streams through a central server. This significantly reduces the bandwidth and CPU load on each participant's device, allowing the conference call to scale to many more people."
|
||||
enable_info2 = Enabling Wide Conference Call will invite the SFU service into this conference call.
|
||||
@@ -0,0 +1,50 @@
|
||||
ul#message_preview {
|
||||
background-color: rgb(var(--movim-background));
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0.75rem 0;
|
||||
}
|
||||
|
||||
ul#message_preview li {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
ul#message_preview li > div.bubble::after {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
ul#message_preview li div.bubble {
|
||||
max-width: calc(100% - 2rem);
|
||||
pointer-events: none;
|
||||
background-color: rgb(var(--movim-background-main));
|
||||
}
|
||||
|
||||
ul#message_preview.list li > div.bubble:not(.file)::before {
|
||||
background-color: rgb(var(--movim-background-main));
|
||||
}
|
||||
|
||||
ul#message_preview li div.bubble:not(.file) {
|
||||
padding-right: 5rem;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 3rem;
|
||||
}
|
||||
|
||||
ul#message_preview li.oppose {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
ul#message_preview li div.bubble div.message > p {
|
||||
white-space: initial;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 4;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
ul#message_preview li.reactions {
|
||||
padding: 0 0.5rem;
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
ul#message_preview li.reactions a.chip {
|
||||
max-width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
var ChatActions = {
|
||||
message: null,
|
||||
|
||||
setMessage: function (message) {
|
||||
ChatActions.message = message;
|
||||
},
|
||||
focusSearch: function () {
|
||||
document.querySelector('form[name=search] input').focus();
|
||||
}
|
||||
}
|
||||
|
||||
MovimEvents.registerWindow('keydown', 'search_message', (e) => {
|
||||
if (e.key == 'f' && e.ctrlKey && MovimUtils.urlParts().page == 'chat' && Chat.getTextarea()) {
|
||||
e.preventDefault();
|
||||
ChatActions_ajaxShowSearchDialog(Chat.getTextarea().dataset.jid, Boolean(Chat.getTextarea().dataset.muc));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
[chatactions]
|
||||
copy_text = Copy the text
|
||||
copied_text = Text copied
|
||||
search_messages = Search messages by content
|
||||
search_messages_empty = No messages found
|
||||
@@ -0,0 +1,518 @@
|
||||
var KeyHelper = libsignal.KeyHelper;
|
||||
|
||||
var ChatOmemo = {
|
||||
refreshed: false,
|
||||
|
||||
initiateBundle: async function (devicesIds) {
|
||||
var store = new ChatOmemoStorage();
|
||||
store.removeAllSessions();
|
||||
|
||||
const identityKeyPair = await KeyHelper.generateIdentityKeyPair();
|
||||
const bundle = {};
|
||||
|
||||
const localDeviceId = await store.getLocalRegistrationId();
|
||||
const deviceId = localDeviceId ?? KeyHelper.generateRegistrationId();
|
||||
|
||||
bundle['identityKey'] = MovimUtils.arrayBufferToBase64(identityKeyPair.pubKey);
|
||||
bundle['deviceId'] = deviceId;
|
||||
|
||||
store.setLocalRegistrationId(deviceId);
|
||||
store.setIdentityKeyPair(identityKeyPair);
|
||||
|
||||
const signedPreKey = await KeyHelper.generateSignedPreKey(identityKeyPair, SIGNED_PREKEY_ID);
|
||||
|
||||
store.storeSignedPreKey(signedPreKey.keyId, signedPreKey);
|
||||
bundle['signedPreKey'] = {
|
||||
'id': signedPreKey.keyId,
|
||||
'publicKey': signedPreKey.keyPair.pubKey,
|
||||
'signature': signedPreKey.signature
|
||||
}
|
||||
const keys = await Promise.all(MovimUtils.range(0, NUM_PREKEYS).map(id => KeyHelper.generatePreKey(id)));
|
||||
keys.forEach(k => store.storePreKey(k.keyId, k.keyPair));
|
||||
|
||||
const preKeys = keys.map(k => ({ 'id': k.keyId, 'key': k.keyPair.pubKey }));
|
||||
bundle['preKeys'] = preKeys;
|
||||
|
||||
ChatOmemo_ajaxNotifyGeneratedBundle();
|
||||
ChatOmemo_ajaxAnnounceBundle(bundle, devicesIds);
|
||||
ChatOmemo_ajaxGetDevicesList(USER_JID);
|
||||
},
|
||||
|
||||
refreshBundle: async function (devicesIds, publishOnly) {
|
||||
var store = new ChatOmemoStorage();
|
||||
|
||||
const bundle = {};
|
||||
const localDeviceId = await store.getLocalRegistrationId();
|
||||
|
||||
if (!localDeviceId) ChatOmemo.initiateBundle(devicesIds);
|
||||
|
||||
// We get the base of the bundle from the store
|
||||
let keyPair = await store.getIdentityKeyPair();
|
||||
const deviceId = localDeviceId;
|
||||
|
||||
bundle['identityKey'] = MovimUtils.arrayBufferToBase64(keyPair.pubKey);
|
||||
bundle['deviceId'] = deviceId;
|
||||
|
||||
let signedPreKey = store.loadCompleteSignedPreKey(SIGNED_PREKEY_ID);
|
||||
bundle['signedPreKey'] = {
|
||||
'id': signedPreKey.keyId,
|
||||
'publicKey': MovimUtils.arrayBufferToBase64(signedPreKey.keyPair.pubKey),
|
||||
'signature': MovimUtils.arrayBufferToBase64(signedPreKey.signature)
|
||||
}
|
||||
|
||||
// We refresh or load all the preKeys
|
||||
let keys = [];
|
||||
let preKeys = [];
|
||||
|
||||
if (publishOnly) {
|
||||
keys = await Promise.all(MovimUtils.range(0, NUM_PREKEYS).map(id => store.loadPreKey(id)));
|
||||
|
||||
let counter = -1;
|
||||
preKeys = keys.map(k => {
|
||||
counter++;
|
||||
return { 'id': counter, 'key': MovimUtils.arrayBufferToBase64(k.pubKey) };
|
||||
});
|
||||
} else {
|
||||
keys = await Promise.all(MovimUtils.range(0, NUM_PREKEYS).map(id => KeyHelper.generatePreKey(id)));
|
||||
keys.forEach(k => store.storePreKey(k.keyId, k.keyPair));
|
||||
preKeys = keys.map(k => ({ 'id': k.keyId, 'key': k.keyPair.pubKey }));
|
||||
}
|
||||
|
||||
bundle['preKeys'] = preKeys;
|
||||
|
||||
ChatOmemo_ajaxAnnounceBundle(bundle, devicesIds);
|
||||
},
|
||||
|
||||
ownDevicesReceived: async function (from, devicesIds) {
|
||||
var store = new ChatOmemoStorage();
|
||||
const localDeviceId = await store.getLocalRegistrationId();
|
||||
devicesIds = Object.values(devicesIds);
|
||||
|
||||
if (localDeviceId == undefined) {
|
||||
ChatOmemo.initiateBundle(devicesIds);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!devicesIds.includes(localDeviceId.toString()) && ChatOmemo.refreshed == false) {
|
||||
ChatOmemo.refreshBundle(devicesIds, true);
|
||||
ChatOmemo.refreshed = true;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* A contact send us a new device list, we check if we can already build a new session
|
||||
*/
|
||||
devicesReceived: async function (from, devicesIds) {
|
||||
var store = new ChatOmemoStorage();
|
||||
let localIds = store.getSessionsIds(from);
|
||||
|
||||
if (await store.getContactState(from)) {
|
||||
for (deviceId of devicesIds.filter(key => !localIds.includes(key))) {
|
||||
ChatOmemo_ajaxGetBundle(from, deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* If we have open sessions from devices that doesn't exists anymore
|
||||
* we close them
|
||||
*/
|
||||
for (deviceId of localIds.filter(key => !devicesIds.includes(key))) {
|
||||
var address = new libsignal.SignalProtocolAddress(from, deviceId);
|
||||
store.removeSession(address);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
bundlesRefreshError(jid) {
|
||||
var store = new ChatOmemoStorage();
|
||||
store.setContactTombstone(jid);
|
||||
ChatOmemo.disableContactState(jid);
|
||||
},
|
||||
|
||||
bundlesRefreshed(jid) {
|
||||
var store = new ChatOmemoStorage();
|
||||
|
||||
if (typeof Chat == 'undefined') return;
|
||||
|
||||
let textarea = Chat && Chat.getTextarea();
|
||||
|
||||
if (textarea) {
|
||||
// First time we handle a session, we enforce the OMEMO state to true by default
|
||||
if (!store.hasContactState(jid)) {
|
||||
store.setContactState(jid, true);
|
||||
}
|
||||
|
||||
if (textarea.dataset.jid == jid) {
|
||||
Chat.setOmemoState('yes');
|
||||
Chat.disableSending();
|
||||
|
||||
Chat_ajaxGet(jid);
|
||||
|
||||
if (textarea.value.length > 0) {
|
||||
Chat.sendMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
handlePreKey: async function (jid, deviceId, preKey) {
|
||||
var store = new ChatOmemoStorage();
|
||||
|
||||
const localDeviceId = await store.getLocalRegistrationId();
|
||||
if (localDeviceId == undefined) return;
|
||||
|
||||
var address = new libsignal.SignalProtocolAddress(jid, deviceId);
|
||||
|
||||
const session = await store.loadSession(address.toString());
|
||||
|
||||
// If we already have a session we don't have to build it
|
||||
if (session) {
|
||||
var promise = Promise.resolve();
|
||||
} else {
|
||||
var sessionBuilder = new libsignal.SessionBuilder(store, address);
|
||||
var promise = sessionBuilder.processPreKey({
|
||||
registrationId: parseInt(deviceId, 10),
|
||||
identityKey: MovimUtils.base64ToArrayBuffer(preKey.identitykey),
|
||||
signedPreKey: {
|
||||
keyId: parseInt(preKey.signedprekeyid, 10),
|
||||
publicKey: MovimUtils.base64ToArrayBuffer(preKey.signedprekeypublic),
|
||||
signature: MovimUtils.base64ToArrayBuffer(preKey.signedprekeysignature)
|
||||
},
|
||||
preKey: {
|
||||
keyId: preKey.prekey.id,
|
||||
publicKey: MovimUtils.base64ToArrayBuffer(preKey.prekey.value)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
promise.then(function onsuccess() {
|
||||
console.log('success ' + jid + ':' + deviceId);
|
||||
|
||||
if (typeof Chat == 'undefined') return;
|
||||
|
||||
let textarea = Chat.getTextarea();
|
||||
if (textarea && textarea.dataset.jid == jid) {
|
||||
Chat.setOmemoState('yes');
|
||||
}
|
||||
});
|
||||
|
||||
promise.catch(function onerror(error) {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
return promise;
|
||||
},
|
||||
|
||||
encrypt: async function (to, plaintext, muc) {
|
||||
var store = new ChatOmemoStorage();
|
||||
|
||||
// https://xmpp.org/extensions/attic/xep-0384-0.3.0.html#usecases-messagesend
|
||||
|
||||
let iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
let key = await crypto.subtle.generateKey(KEY_ALGO, true, ['encrypt', 'decrypt']);
|
||||
|
||||
let algo = {
|
||||
'name': 'AES-GCM',
|
||||
'iv': iv,
|
||||
'tagLength': 128
|
||||
};
|
||||
|
||||
let encrypted = await crypto.subtle.encrypt(algo, key, MovimUtils.stringToArrayBuffer(plaintext));
|
||||
let length = encrypted.byteLength - ((128 + 7) >> 3);
|
||||
let ciphertext = encrypted.slice(0, length);
|
||||
let tag = encrypted.slice(length);
|
||||
let exportedKey = await crypto.subtle.exportKey('raw', key);
|
||||
|
||||
// obj
|
||||
let keyAndTag = MovimUtils.appendArrayBuffer(exportedKey, tag);
|
||||
let biv = MovimUtils.arrayBufferToBase64(iv);
|
||||
let payload = MovimUtils.arrayBufferToBase64(ciphertext);
|
||||
let deviceId = await store.getLocalRegistrationId();
|
||||
let ownKeys = await this.encryptJid(keyAndTag, store.jid);
|
||||
|
||||
let remoteKeys = [];
|
||||
|
||||
if (muc) {
|
||||
for (member of Chat.groupChatMembers) {
|
||||
remoteKeys = remoteKeys.concat(await this.encryptJid(keyAndTag, member));
|
||||
}
|
||||
} else {
|
||||
remoteKeys = await this.encryptJid(keyAndTag, to);
|
||||
}
|
||||
|
||||
ownKeys = ownKeys.concat(remoteKeys);
|
||||
|
||||
let messageKeys = {};
|
||||
ownKeys.map(result => {
|
||||
messageKeys[result.device] = {
|
||||
payload: btoa(result.payload.body),
|
||||
prekey: 3 == parseInt(result.payload.type, 10)
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
'sid': deviceId,
|
||||
'keys': messageKeys,
|
||||
'iv': biv,
|
||||
'payload': payload
|
||||
};
|
||||
},
|
||||
decrypt: async function (message) {
|
||||
if (message.omemoheader == undefined) return;
|
||||
|
||||
var store = new ChatOmemoStorage();
|
||||
|
||||
let resolvedId = message.mine
|
||||
? message.messageid
|
||||
: message.id;
|
||||
|
||||
let maybeDecrypted = await ChatOmemoDB.getMessage(resolvedId);
|
||||
|
||||
if (maybeDecrypted !== undefined) {
|
||||
return maybeDecrypted;
|
||||
}
|
||||
|
||||
// Resolved jid from a muc message
|
||||
var jid = message.mucjid ?? message.jidfrom;
|
||||
|
||||
// If there was an encryption tombstone on this contact we can lift it
|
||||
store.removeContactTombstone(jid);
|
||||
|
||||
// No keys
|
||||
if (message.omemoheader.keys == undefined) return;
|
||||
|
||||
let deviceId = await store.getLocalRegistrationId();
|
||||
|
||||
if (!deviceId) return;
|
||||
|
||||
/**
|
||||
* Check if we need to build more sessions
|
||||
*/
|
||||
if (Chat) {
|
||||
textarea = Chat.getTextarea();
|
||||
|
||||
if (!Boolean(textarea.dataset.muc)) {
|
||||
for (bundleId of Object.keys(message.omemoheader.keys).filter(
|
||||
key => !store.getSessionsIds(jid)
|
||||
.concat(store.getOwnSessionsIds(), [deviceId.toString()]).includes(key)
|
||||
)
|
||||
) {
|
||||
ChatOmemo_ajaxGetBundle(jid, bundleId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (message.omemoheader.keys[deviceId] == undefined) {
|
||||
console.log('Message not encrypted for this device');
|
||||
ChatOmemoDB.putMessage(resolvedId, false);
|
||||
return;
|
||||
}
|
||||
|
||||
let key = message.omemoheader.keys[deviceId];
|
||||
let plainKey;
|
||||
|
||||
try {
|
||||
plainKey = await this.decryptDevice(MovimUtils.base64ToArrayBuffer(key.payload), key.prekey, jid, message.omemoheader.sid);
|
||||
} catch (err) {
|
||||
ChatOmemoDB.putMessage(resolvedId, false);
|
||||
console.log('Error during decryption: ' + err);
|
||||
|
||||
// Let the session heal, a new prekey will be send next time
|
||||
//var address = new libsignal.SignalProtocolAddress(jid, message.omemoheader.sid);
|
||||
//store.removeSession(address);
|
||||
return;
|
||||
}
|
||||
|
||||
let exportedAESKey = plainKey.slice(0, 16);
|
||||
let authenticationTag = plainKey.slice(16);
|
||||
|
||||
if (authenticationTag.byteLength < 16) {
|
||||
if (authenticationTag.byteLength > 0) {
|
||||
throw new Error('Authentication tag too short');
|
||||
}
|
||||
|
||||
console.log(`Authentication tag is only ${authenticationTag.byteLength} byte long`);
|
||||
}
|
||||
|
||||
if (!message.omemoheader.payload) {
|
||||
console.log('No payload to decrypt');
|
||||
}
|
||||
|
||||
// One of our key was used, lets refresh the bundle
|
||||
if (key.prekey) {
|
||||
ChatOmemo.refreshBundle(store.getOwnSessionsIds());
|
||||
}
|
||||
|
||||
let iv = MovimUtils.base64ToArrayBuffer(message.omemoheader.iv);
|
||||
let ciphertextAndAuthenticationTag = MovimUtils.appendArrayBuffer(
|
||||
MovimUtils.base64ToArrayBuffer(message.omemoheader.payload),
|
||||
authenticationTag
|
||||
);
|
||||
|
||||
let importedKey = await crypto.subtle.importKey('raw', exportedAESKey, 'AES-GCM', false, ['decrypt']);
|
||||
let decryptedBuffer = await crypto.subtle.decrypt({
|
||||
name: 'AES-GCM',
|
||||
iv,
|
||||
tagLength: 128
|
||||
}, importedKey, ciphertextAndAuthenticationTag);
|
||||
|
||||
let plaintext = MovimUtils.arrayBufferToString(decryptedBuffer);
|
||||
|
||||
ChatOmemoDB.putMessage(resolvedId, plaintext);
|
||||
return plaintext;
|
||||
},
|
||||
enableContactState: function (jid, muc) {
|
||||
var store = new ChatOmemoStorage();
|
||||
store.setContactState(jid, true);
|
||||
|
||||
if (muc) {
|
||||
Chat_ajaxGetRoom(jid);
|
||||
ChatOmemo_ajaxEnableRoomState();
|
||||
|
||||
Chat.groupChatMembers.forEach(member => {
|
||||
if (!store.isJidResolved(member)) {
|
||||
ChatOmemo_ajaxGetDevicesList(member);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Chat_ajaxGet(jid);
|
||||
ChatOmemo_ajaxEnableContactState();
|
||||
ChatOmemo_ajaxGetDevicesList(jid);
|
||||
}
|
||||
},
|
||||
disableContactState: function (jid, muc) {
|
||||
var store = new ChatOmemoStorage();
|
||||
store.setContactState(jid, false);
|
||||
Chat.setOmemoState("disabled");
|
||||
|
||||
if (muc) {
|
||||
ChatOmemo_ajaxDisableRoomState();
|
||||
} else {
|
||||
ChatOmemo_ajaxDisableContactState();
|
||||
}
|
||||
},
|
||||
getContactState: async function (jid) {
|
||||
var store = new ChatOmemoStorage();
|
||||
return store.getContactState(jid);
|
||||
},
|
||||
encryptJid: function (plaintext, jid) {
|
||||
var store = new ChatOmemoStorage();
|
||||
let promises = store.filter('.session' + jid)
|
||||
.map(key => key.split(/[\s.]+/).pop())
|
||||
.map(deviceId => store.getSessionState(jid + '.' + deviceId).then(state => {
|
||||
if (state) {
|
||||
return this.encryptDevice(plaintext, jid, deviceId);
|
||||
}
|
||||
|
||||
return Promise.resolve(false);
|
||||
}));
|
||||
|
||||
return Promise.all(promises).then(result => {
|
||||
return result.filter(encrypted => encrypted !== false);
|
||||
});
|
||||
},
|
||||
encryptDevice: function (plaintext, jid, deviceId) {
|
||||
var address = new libsignal.SignalProtocolAddress(jid, parseInt(deviceId, 10));
|
||||
var store = new ChatOmemoStorage();
|
||||
var sessionCipher = new libsignal.SessionCipher(store, address);
|
||||
|
||||
return sessionCipher.encrypt(plaintext)
|
||||
.then(payload => ({ 'payload': payload, 'device': deviceId }));
|
||||
},
|
||||
decryptDevice: async function (ciphertext, preKey, jid, deviceId) {
|
||||
var address = new libsignal.SignalProtocolAddress(jid, parseInt(deviceId, 10));
|
||||
var store = new ChatOmemoStorage();
|
||||
var sessionCipher = new libsignal.SessionCipher(store, address);
|
||||
|
||||
return (preKey)
|
||||
? await sessionCipher.decryptPreKeyWhisperMessage(ciphertext, 'binary')
|
||||
: await sessionCipher.decryptWhisperMessage(ciphertext, 'binary');
|
||||
},
|
||||
searchEncryptedFile: function (plaintext) {
|
||||
let lines = plaintext.split('\n');
|
||||
let matches = lines[0].match(AESGCM_REGEX);
|
||||
|
||||
if (!matches) {
|
||||
return plaintext;
|
||||
}
|
||||
let [match, , filename, extension, hash] = matches;
|
||||
return '<i class="material-symbols">file_download</i> <a href="#" class="encrypted_file" onclick="ChatOmemo.getEncryptedFile(\''
|
||||
+ lines[0]
|
||||
+ '\')">'
|
||||
+ filename
|
||||
+ '</a>';
|
||||
},
|
||||
getEncryptedFile: async function (encryptedUrl) {
|
||||
Chat.enableSending();
|
||||
|
||||
let [, url, filename, , hash] = encryptedUrl.match(AESGCM_REGEX);
|
||||
url = 'https://' + url;
|
||||
let response;
|
||||
|
||||
try {
|
||||
response = await fetch(url)
|
||||
} catch (e) {
|
||||
console.log('Cannot get the following file: ' + url)
|
||||
return null;
|
||||
}
|
||||
|
||||
if (response.status >= 200 && response.status < 400) {
|
||||
let cipher = await response.arrayBuffer();
|
||||
const iv = hash.slice(0, 24);
|
||||
const key = hash.slice(24);
|
||||
|
||||
const keyObj = await crypto.subtle.importKey('raw', MovimUtils.hexToArrayBuffer(key), 'AES-GCM', false, ['decrypt']);
|
||||
const algo = {
|
||||
'name': 'AES-GCM',
|
||||
'iv': MovimUtils.hexToArrayBuffer(iv),
|
||||
};
|
||||
let plainFile = await crypto.subtle.decrypt(algo, keyObj, cipher);
|
||||
|
||||
const file = new File([plainFile], filename);
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(file);
|
||||
link.download = filename;
|
||||
link.click();
|
||||
|
||||
Chat.disableSending();
|
||||
|
||||
URL.revokeObjectURL(link.href)
|
||||
}
|
||||
},
|
||||
resolveContactFingerprints: function (jid) {
|
||||
let omemoStorage = new ChatOmemoStorage;
|
||||
|
||||
let sessionLoaded = [];
|
||||
omemoStorage.getSessionsIds(jid).forEach(deviceId => {
|
||||
var address = new libsignal.SignalProtocolAddress(jid, deviceId);
|
||||
sessionLoaded.push(omemoStorage.loadSession(address.toString()));
|
||||
});
|
||||
|
||||
const promise = new Promise((resolve) => {
|
||||
Promise.all(sessionLoaded).then(sessions => {
|
||||
let remoteKeys = [];
|
||||
|
||||
sessions.forEach(session => {
|
||||
let parsed = Object.values(JSON.parse(session).sessions)[0];
|
||||
remoteKeys.push({
|
||||
jid: jid,
|
||||
self: false,
|
||||
bundleid: parsed.registrationId.toString(),
|
||||
fingerprint: btoa(parsed.indexInfo.remoteIdentityKey)
|
||||
});
|
||||
});
|
||||
|
||||
resolve(remoteKeys);
|
||||
});
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
||||
MovimWebsocket.attach(async () => {
|
||||
var store = new ChatOmemoStorage();
|
||||
const localDeviceId = await store.getLocalRegistrationId();
|
||||
if (!localDeviceId) {
|
||||
ChatOmemo_ajaxGetDevicesList(USER_JID);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
var ChatOmemoDB = {
|
||||
dbName: 'movim',
|
||||
storeName: 'decryptedMessages',
|
||||
version: 3,
|
||||
|
||||
cachedMessages: {},
|
||||
|
||||
db: null,
|
||||
|
||||
setup: async function() {
|
||||
var db = window.indexedDB.open(this.dbName, this.version);
|
||||
db.onupgradeneeded = function (event) {
|
||||
var db = event.target.result;
|
||||
db.createObjectStore(ChatOmemoDB.storeName, { keyPath: "id" });
|
||||
}
|
||||
|
||||
db.onsuccess = function() {
|
||||
ChatOmemoDB.db = db;
|
||||
}
|
||||
|
||||
db.onerror = function() {
|
||||
console.log('Movim Decrypted Messages database cannot be opened properly');
|
||||
}
|
||||
},
|
||||
|
||||
loadMessagesByIds: async function(ids) {
|
||||
ChatOmemoDB.cachedMessages = {};
|
||||
|
||||
var tx = ChatOmemoDB.db.result.transaction(ChatOmemoDB.storeName, 'readonly');
|
||||
var os = tx.objectStore(ChatOmemoDB.storeName);
|
||||
|
||||
var promises = [];
|
||||
|
||||
ids.forEach(id => {
|
||||
var request = os.get(id);
|
||||
|
||||
promises.push(new Promise((resolve, reject) => {
|
||||
request.onsuccess = function (event) {
|
||||
if (event.target.result) {
|
||||
ChatOmemoDB.cachedMessages[id] = event.target.result.body;
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
|
||||
request.onerror = function() {
|
||||
resolve();
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
},
|
||||
|
||||
putMessage: async function(id, body) {
|
||||
let promise = new Promise(resolve => {
|
||||
var tx = ChatOmemoDB.db.result.transaction(ChatOmemoDB.storeName, 'readwrite');
|
||||
var os = tx.objectStore(ChatOmemoDB.storeName);
|
||||
|
||||
var request = os.put({"id" : id, "body" : body});
|
||||
request.onsuccess = function(event) {
|
||||
resolve(body);
|
||||
}
|
||||
});
|
||||
|
||||
return promise;
|
||||
},
|
||||
|
||||
getMessage: async function(id) {
|
||||
let promise = new Promise(resolve => {
|
||||
if (id in ChatOmemoDB.cachedMessages) {
|
||||
resolve(ChatOmemoDB.cachedMessages[String(id)]);
|
||||
} else {
|
||||
var tx = ChatOmemoDB.db.result.transaction(ChatOmemoDB.storeName, 'readonly');
|
||||
var os = tx.objectStore(ChatOmemoDB.storeName);
|
||||
|
||||
var request = os.get(id);
|
||||
request.onsuccess = function(event) {
|
||||
if (event.target.result) {
|
||||
resolve(event.target.result.body);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
||||
ChatOmemoDB.setup();
|
||||
@@ -0,0 +1,250 @@
|
||||
function ChatOmemoStorage() {
|
||||
this.jid = USER_JID;
|
||||
}
|
||||
|
||||
ChatOmemoStorage.prototype = {
|
||||
Direction: {
|
||||
SENDING: 1,
|
||||
RECEIVING: 2,
|
||||
},
|
||||
|
||||
// Storage is versionned to ensure a proper reset when changes are made
|
||||
storageVersion: '2',
|
||||
|
||||
// Generic methods
|
||||
put: function (key, value) {
|
||||
if (key === undefined || value === undefined || key === null || value === null)
|
||||
throw new Error("Tried to store undefined/null");
|
||||
|
||||
return localStorage.setObject(this.storageVersion + '.' + key, value);
|
||||
},
|
||||
get: function (key, defaultValue) {
|
||||
if (key === null || key === undefined)
|
||||
throw new Error("Tried to get value for undefined/null key");
|
||||
if (this.storageVersion + '.' + key in localStorage) {
|
||||
return localStorage.getObject(this.storageVersion + '.' + key);
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
},
|
||||
remove: function (key) {
|
||||
if (key === null || key === undefined)
|
||||
throw new Error("Tried to remove value for undefined/null key");
|
||||
|
||||
localStorage.removeItem(this.storageVersion + '.' + key);
|
||||
},
|
||||
filter: function (search) {
|
||||
return Object.keys(localStorage).filter(key => key.startsWith(this.storageVersion + '.' + this.jid + search));
|
||||
},
|
||||
|
||||
// OMEMO specific methods
|
||||
setIdentityKeyPair: function(identityKeyPair) {
|
||||
return Promise.resolve(this.put(this.jid + '.identityKey', {
|
||||
'privKey': MovimUtils.arrayBufferToBase64(identityKeyPair.privKey),
|
||||
'pubKey': MovimUtils.arrayBufferToBase64(identityKeyPair.pubKey)
|
||||
}));
|
||||
},
|
||||
getIdentityKeyPair: function () {
|
||||
identityKeyPair = this.get(this.jid + '.identityKey');
|
||||
|
||||
if (!identityKeyPair) return Promise.reject();
|
||||
|
||||
return Promise.resolve({
|
||||
'privKey': MovimUtils.base64ToArrayBuffer(identityKeyPair.privKey),
|
||||
'pubKey': MovimUtils.base64ToArrayBuffer(identityKeyPair.pubKey)
|
||||
});
|
||||
},
|
||||
|
||||
setLocalRegistrationId: function (registrationId) {
|
||||
return Promise.resolve(this.put(this.jid + '.registrationId', registrationId));
|
||||
},
|
||||
getLocalRegistrationId: function () {
|
||||
return Promise.resolve(this.get(this.jid + '.registrationId'));
|
||||
},
|
||||
|
||||
isTrustedIdentity: function (identifier, identityKey, direction) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
throw new Error("tried to check identity key for undefined/null key");
|
||||
}
|
||||
if (!(identityKey instanceof ArrayBuffer)) {
|
||||
throw new Error("Expected identityKey to be an ArrayBuffer");
|
||||
}
|
||||
var trusted = this.get(this.jid + '.identityKey' + identifier);
|
||||
if (trusted === undefined) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
// movim-rs: identities are stored per JID (not per device) so a contact with several
|
||||
// devices would always be rejected. Blind Trust Before Verification, like Conversations:
|
||||
// the fingerprints can still be checked and disabled in the contact drawer.
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
loadIdentityKey: function (identifier) {
|
||||
if (identifier === null || identifier === undefined)
|
||||
throw new Error("Tried to get identity key for undefined/null key");
|
||||
return Promise.resolve(MovimUtils.base64ToArrayBuffer(this.get(this.jid + '.identityKey' + identifier)));
|
||||
},
|
||||
saveIdentity: function (identifier, identityKey) {
|
||||
if (identifier === null || identifier === undefined)
|
||||
throw new Error("Tried to put identity key for undefined/null key");
|
||||
|
||||
var address = new libsignal.SignalProtocolAddress.fromString(identifier);
|
||||
|
||||
var existing = this.get(this.jid + '.identityKey' + address.getName());
|
||||
this.put(this.jid + '.identityKey' + address.getName(), MovimUtils.arrayBufferToBase64(identityKey))
|
||||
|
||||
if (existing && toString(identityKey) !== toString(existing)) {
|
||||
return Promise.resolve(true);
|
||||
} else {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
loadPreKey: function (keyId) {
|
||||
var res = this.get(this.jid + '.25519KeypreKey' + keyId);
|
||||
if (res !== undefined) {
|
||||
res = {
|
||||
pubKey: MovimUtils.base64ToArrayBuffer(res.pubKey),
|
||||
privKey: MovimUtils.base64ToArrayBuffer(res.privKey)
|
||||
};
|
||||
}
|
||||
return Promise.resolve(res);
|
||||
},
|
||||
storePreKey: function (keyId, keyPair) {
|
||||
keyPair.pubKey = MovimUtils.arrayBufferToBase64(keyPair.pubKey);
|
||||
keyPair.privKey = MovimUtils.arrayBufferToBase64(keyPair.privKey);
|
||||
return Promise.resolve(this.put(this.jid + '.25519KeypreKey' + keyId, keyPair));
|
||||
},
|
||||
removePreKey: function (keyId) {
|
||||
return Promise.resolve(this.remove(this.jid + '.25519KeypreKey' + keyId));
|
||||
},
|
||||
|
||||
loadCompleteSignedPreKey: function (keyId) {
|
||||
var res = this.get(this.jid + '.25519KeysignedKey' + keyId);
|
||||
if (res !== undefined) {
|
||||
res = {
|
||||
keyPair: {
|
||||
pubKey: MovimUtils.base64ToArrayBuffer(res.keyPair.pubKey),
|
||||
privKey: MovimUtils.base64ToArrayBuffer(res.keyPair.privKey),
|
||||
},
|
||||
signature: MovimUtils.base64ToArrayBuffer(res.signature),
|
||||
keyId: keyId
|
||||
};
|
||||
}
|
||||
return res;
|
||||
},
|
||||
loadSignedPreKey: function (keyId) {
|
||||
return Promise.resolve(this.loadCompleteSignedPreKey(keyId).keyPair);
|
||||
},
|
||||
storeSignedPreKey: function (keyId, key) {
|
||||
key.keyPair.pubKey = MovimUtils.arrayBufferToBase64(key.keyPair.pubKey);
|
||||
key.keyPair.privKey = MovimUtils.arrayBufferToBase64(key.keyPair.privKey);
|
||||
key.signature = MovimUtils.arrayBufferToBase64(key.signature);
|
||||
return Promise.resolve(this.put(this.jid + '.25519KeysignedKey' + keyId, key));
|
||||
},
|
||||
removeSignedPreKey: function (keyId) {
|
||||
return Promise.resolve(this.remove(this.jid + '.25519KeysignedKey' + keyId));
|
||||
},
|
||||
|
||||
loadSession: function (identifier) {
|
||||
return Promise.resolve(this.get(this.jid + '.session' + identifier));
|
||||
},
|
||||
storeSession: function (identifier, record) {
|
||||
return Promise.resolve(this.put(this.jid + '.session' + identifier, record));
|
||||
},
|
||||
removeSession: function (identifier) {
|
||||
return Promise.resolve(this.remove(this.jid + '.session' + identifier));
|
||||
},
|
||||
removeAllSessions: function () {
|
||||
let filtered = this.filter('.session');
|
||||
|
||||
for (key in filtered) {
|
||||
localStorage.removeItem(filtered[key]);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
getOwnSessions: function () {
|
||||
return this.filter('.session' + this.jid);
|
||||
},
|
||||
getSessions: function (identifier) {
|
||||
return this.filter('.session' + identifier);
|
||||
},
|
||||
getOwnSessionsIds: function () {
|
||||
return this.filter('.session' + this.jid).map(sessionId => sessionId.split('.').pop());
|
||||
},
|
||||
getSessionsIds: function (identifier) {
|
||||
return this.filter('.session' + identifier).map(sessionId => sessionId.split('.').pop());
|
||||
},
|
||||
|
||||
isJidResolved: function (identifier) {
|
||||
return this.hasContactTombstone(identifier) || this.filter('.session' + identifier).length > 0;
|
||||
},
|
||||
|
||||
removeAllSessionsOfJid: function (identifier) {
|
||||
let filtered = this.filter('.session' + identifier);
|
||||
|
||||
for (id in filtered) {
|
||||
localStorage.removeItem(filtered[id]);
|
||||
}
|
||||
|
||||
this.remove(this.jid + '.contact' + identifier);
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
setContactState: function (jid, state) {
|
||||
return Promise.resolve(this.put(this.jid + '.contact' + jid, state));
|
||||
},
|
||||
removeContactState: function (jid) {
|
||||
return Promise.resolve(this.remove(this.jid + '.contact' + jid));
|
||||
},
|
||||
hasContactState: function(jid) {
|
||||
return (this.get(this.jid + '.contact' + jid) !== undefined);
|
||||
},
|
||||
getContactState: function (jid) {
|
||||
let state = Boolean(this.get(this.jid + '.contact' + jid));
|
||||
return Promise.resolve(state);
|
||||
},
|
||||
|
||||
/**
|
||||
* Tombstone to know if a contact doesn't have OMEMO at the moment
|
||||
*/
|
||||
setContactTombstone: function (jid) {
|
||||
return Promise.resolve(this.put(this.jid + '.contact.tombstone' + jid, true));
|
||||
},
|
||||
removeContactTombstone: function (jid) {
|
||||
return Promise.resolve(this.remove(this.jid + '.contact.tombstone' + jid));
|
||||
},
|
||||
hasContactTombstone: function(jid) {
|
||||
return (this.get(this.jid + '.contact.tombstone' + jid) == true);
|
||||
},
|
||||
|
||||
getSessionState: function (identifier) {
|
||||
return this.loadSession(identifier).then(json => {
|
||||
if (json !== undefined) {
|
||||
let session = JSON.parse(json);
|
||||
return (session.state === true || session.state === undefined);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
},
|
||||
setSessionState: function (identifier, enabled) {
|
||||
this.loadSession(identifier).then(json => {
|
||||
if (json !== undefined) {
|
||||
let session = JSON.parse(json);
|
||||
session.state = Boolean(enabled);
|
||||
this.storeSession(identifier, JSON.stringify(session));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
toString: function(thing) {
|
||||
if (typeof thing == 'string') {
|
||||
return thing;
|
||||
}
|
||||
return new dcodeIO.ByteBuffer.wrap(thing).toString('binary');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
[omemo]
|
||||
fingerprints_title = Fingerprints
|
||||
fingerprints = OMEMO Fingerprints
|
||||
generating_bundle = "Generating the encryption keys, please wait…"
|
||||
generated_bundle = Encryption keys generated
|
||||
resolving_devices = Resolving OMEMO devices
|
||||
building_sessions = Building encrypted sessions
|
||||
building_own_sessions = Building own encryption sessions
|
||||
encrypted = Encrypted message
|
||||
encrypted_disabled = Encryption disabled
|
||||
encrypted_loading = Send a message to build the encrypted session
|
||||
enable_contact = Enabling encryption for the contact
|
||||
enable_room = Enabling encryption for the chatroom
|
||||
disable_contact = Disabling encryption for the contact
|
||||
disable_room = Disabling encryption for the chatroom
|
||||
last_activity = Last activity
|
||||
sessions_built = %s sessions built
|
||||
@@ -0,0 +1,55 @@
|
||||
ul#chats {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
ul#chats ~ .placeholder {
|
||||
display: none;
|
||||
}
|
||||
|
||||
ul#chats li {
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
max-height: 8rem;
|
||||
}
|
||||
|
||||
ul#chats li img.tinythumb {
|
||||
object-fit: cover;
|
||||
width: 1.4rem;
|
||||
height: 1.4rem;
|
||||
margin-right: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
ul#chats li.moving {
|
||||
transition: transform 0.1s, max-height 0.2s var(--accelerate-easing) 0.2s;
|
||||
}
|
||||
|
||||
ul#chats li.closing {
|
||||
transition: transform 0.1s, max-height 0.2s var(--decelerate-easing) 0.2s;
|
||||
max-height: 0;
|
||||
}
|
||||
|
||||
ul#chats li.closing.to_left {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
ul#chats li.closing.to_right {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
ul#chats li.close {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
ul#chats:empty ~ .placeholder {
|
||||
display: block;
|
||||
}
|
||||
|
||||
ul#chats_widget_header[data-scroll] li.subheader:first-child {
|
||||
animation: backgroundBlink 1s;
|
||||
}
|
||||
|
||||
ul#chats_widget_header[data-filter=roster] + ul#chats li:not(.roster) {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
var Chats = {
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
translateX: 0,
|
||||
translateY: 0,
|
||||
slideAuthorized: false,
|
||||
|
||||
setFilter: function (filter) {
|
||||
document.querySelector('#chats_widget_header').dataset.filter = filter;
|
||||
Chats_ajaxSetFilter(filter);
|
||||
},
|
||||
|
||||
refreshFilters: function () {
|
||||
var filter = document.querySelector('#chats_widget_header').dataset.filter;
|
||||
|
||||
document.querySelectorAll('#chats_widget_header span.chip').forEach(chip => {
|
||||
chip.classList.remove('enabled');
|
||||
|
||||
if (chip.dataset.filter == filter) chip.classList.add('enabled');
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function () {
|
||||
var list = document.querySelector('#chats');
|
||||
if (!list) return;
|
||||
|
||||
Chats.refreshFilters();
|
||||
|
||||
var trim = list.innerHTML.trim();
|
||||
|
||||
if (trim === '') list.innerHTML = trim;
|
||||
|
||||
var items = document.querySelectorAll('ul#chats li:not(.subheader)');
|
||||
var i = 0;
|
||||
|
||||
while (i < items.length) {
|
||||
if (items[i].querySelector('img.tinythumb')) {
|
||||
var img = items[i].querySelector('img.tinythumb');
|
||||
|
||||
try {
|
||||
img.src = thumbHashToDataURL(MovimUtils.base64ToBinary(img.dataset.thumbhash));
|
||||
} catch (error) {
|
||||
console.log('Cannot handle thumbhash hash');
|
||||
}
|
||||
}
|
||||
|
||||
if (items[i].dataset.jid != null) {
|
||||
items[i].onclick = function (e) {
|
||||
Rooms.refresh();
|
||||
MovimUtils.addClass('ul#bottomnavigation', 'hidden');
|
||||
|
||||
Chat.get(this.dataset.jid);
|
||||
};
|
||||
|
||||
items[i].onmousedown = function (e) {
|
||||
if (e.which == 2) {
|
||||
Chats.closeItem(this, false);
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
clientWidth = Math.abs(document.body.clientWidth);
|
||||
delay = 20;
|
||||
|
||||
if (MovimUtils.isMobile()) {
|
||||
items[i].addEventListener('touchstart', function (event) {
|
||||
if (MovimTpl.menuDragged) return;
|
||||
|
||||
Chats.startX = event.targetTouches[0].pageX;
|
||||
Chats.startY = event.targetTouches[0].pageY;
|
||||
|
||||
this.classList.remove('moving');
|
||||
}, true);
|
||||
|
||||
items[i].addEventListener('touchmove', function (event) {
|
||||
if (MovimTpl.menuDragged) return;
|
||||
|
||||
Chats.translateX = parseInt(event.targetTouches[0].pageX - Chats.startX);
|
||||
Chats.translateY = parseInt(event.targetTouches[0].pageY - Chats.startY);
|
||||
|
||||
if (Math.abs(Chats.translateX) > delay && Math.abs(Chats.translateX) <= clientWidth) {
|
||||
if (Math.abs(Chats.translateX) > this.offsetWidth / 2) {
|
||||
this.classList.add('close');
|
||||
} else {
|
||||
this.classList.remove('close');
|
||||
}
|
||||
|
||||
if (Math.abs(Chats.translateY) < delay) {
|
||||
Chats.slideAuthorized = true;
|
||||
}
|
||||
|
||||
var moveX = Chats.translateX < 0
|
||||
? (Chats.translateX + delay)
|
||||
: (Chats.translateX - delay);
|
||||
|
||||
if (Chats.slideAuthorized) {
|
||||
this.style.transform = 'translateX(' + (moveX) + 'px)';
|
||||
}
|
||||
} else {
|
||||
this.style.transform = '';
|
||||
this.classList.remove('close');
|
||||
}
|
||||
}, true);
|
||||
|
||||
items[i].addEventListener('touchend', function (event) {
|
||||
if (MovimTpl.menuDragged) return;
|
||||
|
||||
this.classList.add('moving');
|
||||
|
||||
if (Math.abs(Chats.translateX) > this.offsetWidth / 2 && Chats.slideAuthorized) {
|
||||
Chats.closeItem(this, (Chats.translateX < 0));
|
||||
} else {
|
||||
this.classList.remove('close');
|
||||
}
|
||||
|
||||
this.style.transform = '';
|
||||
Chats.slideAuthorized = false;
|
||||
Chats.startX = Chats.startY = Chats.translateX = Chats.translateY = 0;
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
},
|
||||
|
||||
clearAllActives: function () {
|
||||
document.querySelectorAll('ul#chats li:not(.subheader)')
|
||||
.forEach(item => item.classList.remove('active'));
|
||||
},
|
||||
|
||||
setActive: function (jid) {
|
||||
Chats.clearAllActives();
|
||||
Rooms.clearAllActives();
|
||||
MovimUtils.addClass('ul#chats li[data-jid="' + jid + '"]', 'active');
|
||||
},
|
||||
|
||||
closeItem(li, toLeft) {
|
||||
li.classList.add('closing');
|
||||
li.classList.add(toLeft ? 'to_left' : 'to_right');
|
||||
|
||||
window.setTimeout(() => {
|
||||
Chats_ajaxClose(li.dataset.jid, (MovimUtils.urlParts().params[0] === li.dataset.jid));
|
||||
}, 400);
|
||||
}
|
||||
};
|
||||
|
||||
MovimEvents.registerWindow('loaded', 'chats', () => Chats.refresh());
|
||||
|
||||
MovimWebsocket.initiate(() => Chats_ajaxHttpGet());
|
||||
@@ -0,0 +1,13 @@
|
||||
[chats]
|
||||
empty_title = No chats yet…
|
||||
empty = Start a new conversation by clicking on the %s button.
|
||||
new_title = Start a new conversation
|
||||
add = Chat with a contact
|
||||
me = Me
|
||||
picture = Picture
|
||||
video = Video
|
||||
audio = Audio
|
||||
|
||||
[chats_filter]
|
||||
all = All
|
||||
roster = Contacts
|
||||
@@ -0,0 +1,30 @@
|
||||
var Communities = {
|
||||
init: function() {
|
||||
let parts = MovimUtils.urlParts();
|
||||
let page = parts.params[0] !== undefined ? parseInt(parts.params[0]) : 0;
|
||||
|
||||
if (parts.hash != '') {
|
||||
switch (parts.hash) {
|
||||
case 'communities':
|
||||
Communities_ajaxHttpGetCommunities(page);
|
||||
break;
|
||||
case 'contacts':
|
||||
Communities_ajaxHttpGetContacts(page);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
Communities_ajaxHttpGetAll(page);
|
||||
}
|
||||
},
|
||||
|
||||
morePosts: function(button, page, type) {
|
||||
button.remove();
|
||||
Communities_ajaxHttpGetPosts(page, type);
|
||||
}
|
||||
}
|
||||
|
||||
MovimWebsocket.attach(function() {
|
||||
Notif.current('community');
|
||||
});
|
||||
|
||||
MovimWebsocket.initiate(() => Communities.init());
|
||||
@@ -0,0 +1,8 @@
|
||||
[communities]
|
||||
empty_text = Discover, follow and share
|
||||
counter = "%s communities"
|
||||
search_server = Search for a new server
|
||||
servers = Communities servers
|
||||
servers_text = Discover the Communities by servers
|
||||
disco = Communities server discovered
|
||||
disco_error = "This server doesn’t exists"
|
||||
@@ -0,0 +1,2 @@
|
||||
[communitiesinteresting]
|
||||
about = Communities you might like
|
||||
@@ -0,0 +1,7 @@
|
||||
MovimWebsocket.attach(function() {
|
||||
Notif.current('community');
|
||||
var parts = MovimUtils.urlParts();
|
||||
if (parts.params.length > 0) {
|
||||
CommunitiesServer_ajaxDisco(parts.params[0]);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
[communitiesserver]
|
||||
created = Community created successfully
|
||||
empty_server = "There is no Communities yet on this server."
|
||||
add = Create a new community on %s
|
||||
name = Community name
|
||||
name_example = My Little Pony - Fan Club
|
||||
name_error = Please provide a valid community name (4 characters minimum)
|
||||
no_creation = You cannot create a new community on this server
|
||||
disco_error = Issue when trying to discover the items
|
||||
remoteservernotfound = "The server %s cannot be reached"
|
||||
@@ -0,0 +1,5 @@
|
||||
MovimWebsocket.initiate(() => CommunitiesServers_ajaxHttpGet());
|
||||
|
||||
MovimWebsocket.attach(function () {
|
||||
Notif.current('explore');
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
[communitiesservers]
|
||||
empty_title = No Communities Servers yet…
|
||||
empty_text = "It seems that we can't find any communities for the moment"
|
||||
@@ -0,0 +1,20 @@
|
||||
var CommunityAffiliations = {
|
||||
update : function(jid) {
|
||||
var parts = MovimUtils.urlParts();
|
||||
if (parts.params.length > 0) {
|
||||
CommunityAffiliations_ajaxChangeAffiliation(
|
||||
parts.params[0],
|
||||
parts.params[1],
|
||||
MovimUtils.formToJson(jid)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MovimWebsocket.attach(function() {
|
||||
var parts = MovimUtils.urlParts();
|
||||
if (parts.params.length > 0) {
|
||||
CommunityAffiliations_ajaxGetSubscriptions(parts.params[0], parts.params[1]);
|
||||
CommunityAffiliations_ajaxGetAffiliations(parts.params[0], parts.params[1]);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
[communityaffiliation]
|
||||
configuration = Configuration
|
||||
subscriptions = Subscriptions
|
||||
public_subscriptions= Public subscriptions
|
||||
subscriptions_text = My Communities subscriptions
|
||||
deleted = Community deleted successfully
|
||||
delete_title = Delete the community
|
||||
delete_text = You are going to delete the following community. Please confirm your action.
|
||||
delete_clean_text = "It seems that this community doesn’t exists anymore. Do you want to remove it from your subscriptions?"
|
||||
delete_error = Error during the node deletion
|
||||
config_saved = Configuration saved
|
||||
config_error = Error during the application of the node configuration
|
||||
owners = Owners
|
||||
publishers = Publishers
|
||||
roles = Roles
|
||||
role_set = Role correctly set
|
||||
@@ -0,0 +1,14 @@
|
||||
[communityconfig]
|
||||
description = Description
|
||||
publication = Publication
|
||||
publish_model_open_title = Public
|
||||
publish_model_open_text = Everyone can publish
|
||||
publish_model_publishers_title = Publishers
|
||||
publish_model_publishers_text = The publishers can publish
|
||||
publish_model_subscribers_title = Subscribers
|
||||
publish_model_subscribers_text = The subscribers can publish
|
||||
type = Community type
|
||||
type_articles_title = Articles
|
||||
type_articles_text = Publish and read articles
|
||||
type_gallery_title = Gallery
|
||||
type_gallery_text = Publish and browse pictures
|
||||
@@ -0,0 +1,4 @@
|
||||
#community_data li:first-child img.avatar,
|
||||
#community_data li:first-child span.avatar {
|
||||
display: inline-block;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
MovimWebsocket.attach(function() {
|
||||
if (!MovimUtils.isMobile()) {
|
||||
var parts = MovimUtils.urlParts();
|
||||
if (parts.params.length > 0) {
|
||||
CommunityData_ajaxGetAvatar(parts.params[0], parts.params[1]);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
[communitydata]
|
||||
sub = %s subscribers
|
||||
num = %s posts
|
||||
public = Public page
|
||||
publishmodel_publishers = Only publishers can publish
|
||||
publishmodel_subscribers = Only subscribers can publish
|
||||
@@ -0,0 +1,13 @@
|
||||
var CommunityHeader = {
|
||||
getMetadata: function () {
|
||||
var parts = MovimUtils.urlParts();
|
||||
if (parts.params.length > 0) {
|
||||
CommunityHeader_ajaxGetMetadata(parts.params[0], parts.params[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MovimWebsocket.attach(function () {
|
||||
Notif.current('community');
|
||||
CommunityHeader.getMetadata();
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
[communityheader]
|
||||
follow = Follow
|
||||
unfollow = Unfollow
|
||||
followed = Followed
|
||||
unfollowed = Unfollowed
|
||||
subscription_unsupported = The subscriptions are disabled for this community
|
||||
label_label = Give a label for this community
|
||||
label_placeholder = My Community Name
|
||||
share_follow = Share this follow to everyone
|
||||
share_follow_text = It will appear on your profile
|
||||
unfollow_text = You are going to unfollow this community
|
||||
sure = Are you sure?
|
||||
@@ -0,0 +1,16 @@
|
||||
var CommunityPosts = {
|
||||
getItems: function () {
|
||||
var parts = MovimUtils.urlParts();
|
||||
if (parts.params.length > 0) {
|
||||
var node = (parts.params[1] == undefined || parts.params[1] == '')
|
||||
? 'urn:xmpp:microblog:0'
|
||||
: parts.params[1];
|
||||
|
||||
CommunityPosts_ajaxGetItems(parts.params[0], node, parts.params[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MovimWebsocket.attach(function () {
|
||||
CommunityPosts.getItems();
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
[communityposts]
|
||||
empty_me_text = No publication yet? Write your first post to make people discover your profile
|
||||
empty_me_button = Publish my first post
|
||||
presencerequired = Private account
|
||||
presencerequired_text = Ask %s to be in your contacts to see the publications
|
||||
subscribe_presencerequired = Send an invitation request before following the contact
|
||||
subscribe_unsupported = "You can't follow the contact at the moment"
|
||||
@@ -0,0 +1,5 @@
|
||||
@media screen and (max-width: 1024px) {
|
||||
#subscriptions {
|
||||
padding: 0 0.75rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
MovimWebsocket.attach(() => CommunitySubscriptions_ajaxHttpGet());
|
||||
@@ -0,0 +1,5 @@
|
||||
[communitysubscriptions]
|
||||
subscriptions = Subscribed Communities
|
||||
empty_title = Hello
|
||||
empty_text1 = Here you will be able to manage all your subscriptions.
|
||||
empty_text2 = You don’t have any community subscriptions yet.
|
||||
@@ -0,0 +1,39 @@
|
||||
label[for=nightmode] i,
|
||||
form ul.list li div>span.supporting.night_mode_detected {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
form > div .checkbox > input[type="checkbox"] + label[for=nightmode] {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
left: 1.25em;
|
||||
top: 0.25em;
|
||||
}
|
||||
|
||||
label[for=nightmode] i,
|
||||
form ul.list li div>span.supporting.night_mode_detected {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
form div:has(> #nightmode) {
|
||||
opacity: 0.25;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
form li#accent_color div.radio {
|
||||
display: inline-block;
|
||||
margin-right: 0.5rem;
|
||||
line-height: 5rem;
|
||||
}
|
||||
|
||||
form li#accent_color div.radio label {
|
||||
transform: scale(1.5);
|
||||
position: relative;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
form li#accent_color div.radio input[type=radio] {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
var Config = {
|
||||
switchNightMode: function () {
|
||||
document.body.classList.toggle('nightmode');
|
||||
},
|
||||
|
||||
setAccentColor: function (color) {
|
||||
document.body.style.setProperty('--movim-accent', 'var(--p-' + color + ')');
|
||||
},
|
||||
|
||||
updateSystemVariable: function (variable, value) {
|
||||
window[variable] = value;
|
||||
}
|
||||
}
|
||||
|
||||
MovimWebsocket.attach(function () {
|
||||
Notif.current('configuration');
|
||||
|
||||
Config_ajaxMAMGetConfig();
|
||||
Config_ajaxBlogGetConfig();
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
[config]
|
||||
general = General
|
||||
language = Language
|
||||
roster = Roster display
|
||||
roster_show = Show the offline contacts
|
||||
roster_hide = Hide the offline contacts
|
||||
appearence = Appearance
|
||||
updated = Configuration updated
|
||||
not_valid = Configuration invalid
|
||||
advanced = Advanced Configuration
|
||||
nsfw = Display adult content
|
||||
nsfw_text = Will allow you to read the posts tagged with the #nsfw tag
|
||||
omemoenabled = Use OMEMO
|
||||
omemoenabled_text = Enable end to end encryption on Movim for your account
|
||||
omemoenabled_warning = "To encrypt and decrypt messages Movim is using some code delivered from the %s web domain. If you want to exchange sensitive information please ensure that you fully trust the code issued by this intermediary."
|
||||
night_mode = Night Mode
|
||||
night_mode_text = Use dark colors to rest your eyes
|
||||
night_mode_detected = Your browser is already telling Movim to use the Night Mode by default
|
||||
chatmain = Main chat page
|
||||
chatmain_text = Set the chat page to the default when opening Movim
|
||||
mam_text = Server-side chat messages archiving preferences
|
||||
mam_always = Always
|
||||
mam_never = Never
|
||||
mam_saved = Archiving preferences saved
|
||||
blog_text = Personal blog confidentiality
|
||||
blog_open_title = Public blog
|
||||
blog_open_text = Accessible to everyone on the network
|
||||
blog_presence_title = Private blog
|
||||
blog_presence_text = Restricted to your subscribers only
|
||||
blog_saved = Personal blog confidentiality saved
|
||||
confidentiality = Confidentiality
|
||||
audio_title = Audio notifications
|
||||
audio_call = Incoming call
|
||||
audio_chat = Incoming message
|
||||
accent_color = Accent color
|
||||
change_nickname_title= Be careful
|
||||
change_nickname_text = Changing your nickname will change your public blog, feed, and articles URLs
|
||||
@@ -0,0 +1,4 @@
|
||||
#confirm ul h2,
|
||||
#confirm ul h3 {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
[confirm]
|
||||
title = Confirmation of authentication
|
||||
description = The following platform would like your confirmation to authentify this account
|
||||
code = Authentication code
|
||||
@@ -0,0 +1,48 @@
|
||||
var ContactActions = {
|
||||
getDrawerFingerprints: function (jid) {
|
||||
ChatOmemo.resolveContactFingerprints(jid).then(remoteKeys => {
|
||||
ContactActions_ajaxGetDrawerFingerprints(jid, remoteKeys);
|
||||
});
|
||||
},
|
||||
morePictures(button, jid, page) {
|
||||
button.remove();
|
||||
ContactActions_ajaxHttpGetPictures(jid, page);
|
||||
},
|
||||
moreLinks(button, jid, page) {
|
||||
button.remove();
|
||||
ContactActions_ajaxHttpGetLinks(jid, page);
|
||||
},
|
||||
resolveSessionsStates: function (jid, room = false) {
|
||||
var store = new ChatOmemoStorage();
|
||||
|
||||
store.getSessionsIds(jid).map(id => {
|
||||
store.getSessionState(jid + '.' + id).then(state => {
|
||||
if (state) {
|
||||
let icon = document.querySelector('span#sessionicon_' + MovimUtils.cleanupId(jid) + '_' + id);
|
||||
if (icon) {
|
||||
icon.classList.remove('blue');
|
||||
icon.classList.add('blue');
|
||||
}
|
||||
|
||||
let checkbox = document.querySelector('input[name=sessionstate_' + MovimUtils.cleanupId(jid) + '_' + id + ']');
|
||||
|
||||
if (checkbox) {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (room == false) {
|
||||
store.getContactState(jid).then(enabled => {
|
||||
if (!enabled && document.querySelector('#omemo_fingerprints ul.list')) {
|
||||
document.querySelector('#omemo_fingerprints ul.list').classList.add('disabled');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
toggleFingerprintState: function (checkbox) {
|
||||
var store = new ChatOmemoStorage();
|
||||
let set = store.setSessionState(checkbox.dataset.identifier, checkbox.checked);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
[roster]
|
||||
add_title = Add a contact
|
||||
add_contact_info1 = Enter the XMPP Address of your contact.
|
||||
add_contact_info2 = Press enter to validate.
|
||||
added = Contact added
|
||||
updated = Contact updated
|
||||
deleted = Contact deleted
|
||||
@@ -0,0 +1,3 @@
|
||||
MovimWebsocket.attach(() => {
|
||||
ContactBlogConfig_ajaxCheckAccessModel(MovimUtils.urlParts().params[0]);
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
MovimWebsocket.attach(function() {
|
||||
Notif.current('contact');
|
||||
|
||||
if (!MovimUtils.isMobile()) {
|
||||
var parts = MovimUtils.urlParts();
|
||||
|
||||
ContactData_ajaxGet(parts.params[0]);
|
||||
|
||||
if (parts.params.length > 0) {
|
||||
ContactData_ajaxRefresh(parts.params[0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
[general]
|
||||
legend = General Information
|
||||
name = Name
|
||||
pronouns = Pronouns
|
||||
pronouns_example = "he/she/they/…"
|
||||
nickname = Nickname
|
||||
date_of_birth = Date of Birth
|
||||
gender = Gender
|
||||
marital = Marital Status
|
||||
email = Email
|
||||
website = Website
|
||||
about = About Me
|
||||
accounts = Other Accounts
|
||||
tune = Is Listening
|
||||
pictures = Pictures
|
||||
links = Links
|
||||
|
||||
private_account_title = Your account is private
|
||||
private_account_text = Go to the Settings page to create a public blog and allow the other users to find you
|
||||
|
||||
[position]
|
||||
legend = Geographic Position
|
||||
locality = Locality
|
||||
country = Country
|
||||
|
||||
[listen]
|
||||
title = Listening
|
||||
|
||||
[last]
|
||||
title = Last seen
|
||||
|
||||
[clients]
|
||||
title = Clients
|
||||
title_full = Clients information
|
||||
|
||||
[explore]
|
||||
last_registered = Last registered
|
||||
explore = Find some new contacts
|
||||
|
||||
[edit]
|
||||
title = Edit
|
||||
alias = Alias
|
||||
group = Group
|
||||
|
||||
[delete]
|
||||
title = Are you sure?
|
||||
text = You are going to remove one of your contacts, please confirm your action
|
||||
|
||||
[age]
|
||||
years = %s years
|
||||
|
||||
[blog]
|
||||
last = Last public post
|
||||
visit = Visit the public blog
|
||||
|
||||
[subscription]
|
||||
to = You can see this contact status
|
||||
to_button = Share my status
|
||||
to_text = But this contact cannot see yours
|
||||
from = You are not subscribed to this contact
|
||||
from_button = Ask to subscribe
|
||||
from_text = But this contact can still see if you are online
|
||||
nil = You are not subscribed to anyone
|
||||
nil_button = Invite
|
||||
nil_text = This contact is in your contact list but you are not subscribed to each other
|
||||
@@ -0,0 +1,6 @@
|
||||
var ContactHeader = {
|
||||
submit: function (button) {
|
||||
button.innerHTML = '<i class="material-symbols spin">progress_activity</i>';
|
||||
button.classList.add('inactive');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
MovimWebsocket.attach(function() {
|
||||
if (!MovimUtils.isMobile()) {
|
||||
var parts = MovimUtils.urlParts();
|
||||
if (parts.params.length > 0) {
|
||||
ContactSubscriptions_ajaxRefresh(parts.params[0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
var ContactsSuggestions = {
|
||||
submit: function (button) {
|
||||
button.innerHTML = '<i class="material-symbols spin">progress_activity</i>';
|
||||
button.classList.add('inactive');
|
||||
}
|
||||
};
|
||||
|
||||
if (!MovimUtils.isMobile()) {
|
||||
ContactsSuggestions_ajaxHttpGet();
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
[contactssuggestions]
|
||||
for_you = Suggestions for you
|
||||
@@ -0,0 +1,35 @@
|
||||
var Dialog = {
|
||||
addScroll : function() {
|
||||
document.querySelector('#dialog').classList.add('scroll');
|
||||
},
|
||||
removeScroll : function() {
|
||||
document.querySelector('#dialog').classList.remove('scroll');
|
||||
},
|
||||
addLocked : function() {
|
||||
document.querySelector('#dialog').classList.add('locked');
|
||||
},
|
||||
locked : function() {
|
||||
return document.querySelector('#dialog').classList.contains('locked');
|
||||
},
|
||||
filled : function() {
|
||||
return (document.querySelector('#dialog').innerHTML != '');
|
||||
},
|
||||
clear : function() {
|
||||
if (Dialog.filled()
|
||||
&& !Dialog.locked()) {
|
||||
Dialog_ajaxClear();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
MovimEvents.registerBody('click', 'dialog', (e) => {
|
||||
if (Dialog.filled() && document.querySelector('body') == e.target) {
|
||||
Dialog.clear();
|
||||
}
|
||||
});
|
||||
|
||||
MovimEvents.registerBody('keydown', 'dialog', (e) => {
|
||||
if (e.key == 'Escape') {
|
||||
Dialog.clear();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
#dictaphone_widget {
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
#dictaphone_widget.show {
|
||||
display: block;
|
||||
border-top: 1px solid rgba(var(--movim-font), 0.12);
|
||||
}
|
||||
|
||||
#dictaphone_widget li {
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
#dictaphone_widget li #play_pause:not(.enabled) {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#dictaphone_widget li #play_pause:not(.enabled) ~ #audio_upload {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#dictaphone_widget li div {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#dictaphone_widget li div #timer {
|
||||
width: 13rem;
|
||||
line-height: 5rem;
|
||||
}
|
||||
|
||||
#dictaphone_widget li div p.rec {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
filter: saturate(0);
|
||||
line-height: 5rem;
|
||||
}
|
||||
|
||||
#dictaphone_widget li div input[type=range] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#dictaphone_widget li #play_pause:not(.enabled) ~ div input[type=range] {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1024px) {
|
||||
#dictaphone_widget li div input[type=range] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#dictaphone_widget li div #timer {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#dictaphone_widget li div p.rec:before {
|
||||
font-family: 'Material Symbols';
|
||||
content: "\e061";
|
||||
vertical-align: bottom;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
#dictaphone_widget li span#record:active ~ div p.rec {
|
||||
filter: saturate(1);
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
var Dictaphone = {
|
||||
mediaRecorder: null,
|
||||
audio: null,
|
||||
progressBar: null,
|
||||
playPause: null,
|
||||
audioUpload: null,
|
||||
timer: null,
|
||||
timerInterval: null,
|
||||
recordTimeMs: 0,
|
||||
recordTimerStart: null,
|
||||
chunks: [],
|
||||
audioStream: null,
|
||||
|
||||
toggle: function () {
|
||||
let classList = document.querySelector('#dictaphone_widget').classList;
|
||||
if (classList.contains('show')) {
|
||||
classList.remove('show');
|
||||
Dictaphone.clear();
|
||||
} else {
|
||||
classList.add('show');
|
||||
Chat.scrollTotally();
|
||||
}
|
||||
},
|
||||
|
||||
init: function () {
|
||||
Dictaphone.audio = document.querySelector('#dictaphone_widget audio');
|
||||
Dictaphone.progressBar = document.querySelector('#dictaphone_widget input[type=range]');
|
||||
Dictaphone.playPause = document.querySelector('#dictaphone #play_pause');
|
||||
Dictaphone.audioUpload = document.querySelector('#dictaphone #audio_upload');
|
||||
Dictaphone.timer = document.querySelector('#dictaphone_widget #timer');
|
||||
const record = document.querySelector('#dictaphone_widget #record');
|
||||
record.onmousedown = Dictaphone.record;
|
||||
record.onmouseup = Dictaphone.stop;
|
||||
|
||||
Dictaphone.updateRecordTimer();
|
||||
},
|
||||
|
||||
clear: function () {
|
||||
Dictaphone.audio.src = '';
|
||||
Dictaphone.playPause.classList.remove('enabled');
|
||||
Dictaphone.recordTimeMs = 0;
|
||||
Dictaphone.progressBar.value = 0;
|
||||
Dictaphone.updateRecordTimer();
|
||||
},
|
||||
|
||||
record: function () {
|
||||
const constraints = { audio: true };
|
||||
|
||||
if (localStorage.getItem('defaultMicrophone')) {
|
||||
constraints.audio = {
|
||||
deviceId: localStorage.getItem('defaultMicrophone')
|
||||
}
|
||||
}
|
||||
|
||||
let mouseDownOnSlider = false;
|
||||
|
||||
navigator.mediaDevices.getUserMedia(constraints).then(audioStream => {
|
||||
Dictaphone.audioStream = audioStream;
|
||||
|
||||
Dictaphone.mediaRecorder = new MediaRecorder(audioStream, { mimeType: 'audio/webm; codecs=opus' });
|
||||
|
||||
Dictaphone.mediaRecorder.ondataavailable = function (e) {
|
||||
Dictaphone.chunks.push(e.data);
|
||||
}
|
||||
|
||||
Dictaphone.mediaRecorder.onstart = function (e) {
|
||||
Dictaphone.clear();
|
||||
|
||||
Dictaphone.recordTimerStart = new Date();
|
||||
Dictaphone.timerInterval = setInterval(e => {
|
||||
Dictaphone.updateRecordTimer();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
Dictaphone.mediaRecorder.onstop = function (e) {
|
||||
const blob = new Blob(Dictaphone.chunks);
|
||||
Dictaphone.chunks = [];
|
||||
|
||||
Dictaphone.audio.src = window.URL.createObjectURL(blob);
|
||||
|
||||
Upload.prepare(new File([blob], "record.opus", { type: 'audio/ogg'}));
|
||||
Upload.name = 'record.opus';
|
||||
|
||||
Dictaphone.recordTimeMs = new Date() - Dictaphone.recordTimerStart;
|
||||
Dictaphone.recordTimerStart = null;
|
||||
|
||||
clearInterval(Dictaphone.timerInterval);
|
||||
|
||||
Dictaphone.updateRecordTimer();
|
||||
}
|
||||
|
||||
Dictaphone.audioUpload.onclick = function () {
|
||||
Upload.init();
|
||||
Dictaphone.clear();
|
||||
}
|
||||
|
||||
Dictaphone.audio.onloadeddata = function () {
|
||||
Dictaphone.playPause.classList.add('enabled');
|
||||
Dictaphone.progressBar.value = 0;
|
||||
Dictaphone.updateRecordTimer();
|
||||
}
|
||||
|
||||
Dictaphone.audio.ontimeupdate = function () {
|
||||
if (!mouseDownOnSlider) {
|
||||
Dictaphone.progressBar.value = Dictaphone.audio.currentTime / Dictaphone.recordTimeMs * 100 * 1000;
|
||||
}
|
||||
|
||||
Dictaphone.updateRecordTimer();
|
||||
}
|
||||
|
||||
Dictaphone.audio.onplay = function () {
|
||||
Dictaphone.playPause.querySelector('i').innerHTML = 'pause';
|
||||
};
|
||||
|
||||
Dictaphone.audio.onpause = function () {
|
||||
Dictaphone.playPause.querySelector('i').innerHTML = 'play_arrow';
|
||||
};
|
||||
|
||||
Dictaphone.progressBar.onchange = function () {
|
||||
Dictaphone.audio.currentTime = Dictaphone.recordTimeMs / 100000 * Dictaphone.progressBar.value;
|
||||
}
|
||||
|
||||
Dictaphone.playPause.onclick = function () {
|
||||
if (Dictaphone.audio.paused) {
|
||||
Dictaphone.audio.play();
|
||||
} else {
|
||||
Dictaphone.audio.pause();
|
||||
}
|
||||
};
|
||||
|
||||
Dictaphone.progressBar.onmousedown = function() {
|
||||
mouseDownOnSlider = true;
|
||||
}
|
||||
|
||||
Dictaphone.progressBar.onmouseup = function() {
|
||||
mouseDownOnSlider = false;
|
||||
}
|
||||
|
||||
Dictaphone.mediaRecorder.start();
|
||||
}, error => console.log(error));
|
||||
},
|
||||
|
||||
updateRecordTimer: function () {
|
||||
Dictaphone.timer.innerHTML = MovimUtils.cleanTime(Dictaphone.audio.currentTime);
|
||||
Dictaphone.timer.innerHTML += ' / ';
|
||||
Dictaphone.timer.innerHTML +=
|
||||
(Dictaphone.recordTimerStart != null)
|
||||
? MovimUtils.cleanTime(Math.round((new Date() - Dictaphone.recordTimerStart) / 1000))
|
||||
: MovimUtils.cleanTime(Math.round(Dictaphone.recordTimeMs / 1000));
|
||||
},
|
||||
|
||||
stop: function () {
|
||||
if (Dictaphone.mediaRecorder) {
|
||||
Dictaphone.mediaRecorder.stop();
|
||||
}
|
||||
|
||||
Dictaphone.audioStream.getTracks().forEach(function (track) {
|
||||
track.stop();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
[dictaphone]
|
||||
name = Dictaphone
|
||||
push_to_record = Push to record
|
||||
recording = Recording
|
||||
@@ -0,0 +1,131 @@
|
||||
#draw {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
display: none;
|
||||
|
||||
backdrop-filter: blur(1rem);
|
||||
}
|
||||
|
||||
#draw:before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0.75;
|
||||
background-color: rgb(var(--movim-background));
|
||||
}
|
||||
|
||||
#draw.open {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#draw .canvas {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
#draw ul.list.controls li {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#draw #draw-background {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
#draw #screen-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
#draw #draw-canvas {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
/* Controls */
|
||||
|
||||
#draw .draw-top-nav {
|
||||
transition: transform 0.2s;
|
||||
pointer-events: none;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
#draw .draw-top-nav span {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
#draw .draw-top-nav span.control {
|
||||
margin-right: 1.5rem;
|
||||
box-shadow: var(--elevation-1);
|
||||
}
|
||||
|
||||
#draw .draw-actions,
|
||||
#draw .draw-control {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
#draw .draw-actions {
|
||||
left: -1rem;
|
||||
right: initial;
|
||||
}
|
||||
|
||||
#draw .draw-top-nav.drawing {
|
||||
transform: translateY(-130%);
|
||||
}
|
||||
|
||||
#draw .draw-control.drawing {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
#draw .draw-actions.drawing {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
#draw .draw-control span.icon.bubble i,
|
||||
#draw .draw-actions span.icon.bubble i {
|
||||
line-height: 5rem;
|
||||
height: 5rem;
|
||||
}
|
||||
|
||||
#draw .draw-actions li span.primary,
|
||||
#draw .draw-control li span.primary {
|
||||
height: 5rem;
|
||||
flex: 0 0 5rem;
|
||||
margin: 0.5rem 1rem;
|
||||
transition: box-shadow .3s cubic-bezier(.4,0,.2,1), transform .3s cubic-bezier(.4,0,.2,1);
|
||||
box-shadow: var(--elevation-1);
|
||||
}
|
||||
|
||||
#draw .draw-actions li span.primary:hover,
|
||||
#draw .draw-control li span.primary:hover {
|
||||
box-shadow: var(--elevation-2);
|
||||
}
|
||||
|
||||
#draw .draw-actions li.selected span.primary,
|
||||
#draw .draw-control li.selected span.primary {
|
||||
box-shadow: var(--elevation-4);
|
||||
transform: scale(1.3);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#draw [data-width=small] i {
|
||||
font-size: 2rem;
|
||||
}
|
||||
#draw [data-width=medium] i {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
var Draw = {
|
||||
initCanvas: null,
|
||||
|
||||
SMALL: 4,
|
||||
MEDIUM: 6,
|
||||
BIG: 8,
|
||||
|
||||
CROP_PADDING: 0.15,
|
||||
|
||||
draw: null, // widget wrapper
|
||||
|
||||
canvas: null, // drawable
|
||||
ctx: null,
|
||||
bg: null, // non drawable, only used for background
|
||||
|
||||
drawing: false,
|
||||
mousePos: null,
|
||||
lastPos: null,
|
||||
|
||||
drawingData: null, // data structure for saving drawn points
|
||||
|
||||
control: null,
|
||||
actions: null,
|
||||
topNav: null,
|
||||
save: null, // button
|
||||
|
||||
backgroundCanvas: null, // background coming from another widget as a canvas
|
||||
bgHeight: null, // natural height of background
|
||||
bgWidth: null, // natural width of background
|
||||
ratio: 1, // upscale ratio
|
||||
|
||||
init: function () {
|
||||
MovimTpl.pushAnchorState('draw', function () {
|
||||
Draw.draw.classList.remove('bound', 'open');
|
||||
});
|
||||
|
||||
Draw.drawingData = [];
|
||||
|
||||
if (Draw.initCanvas) {
|
||||
Draw.backgroundCanvas = Draw.initCanvas;
|
||||
Draw.initCanvas = null;
|
||||
} else {
|
||||
Draw.backgroundCanvas = null;
|
||||
}
|
||||
|
||||
Draw.topNav = document.querySelector('.draw-top-nav');
|
||||
Draw.control = document.querySelector('.draw-control');
|
||||
Draw.actions = document.querySelector('.draw-actions');
|
||||
|
||||
Draw.draw = document.getElementById('draw');
|
||||
const canvasWrapper = document.querySelector('#draw .canvas');
|
||||
const colors = document.querySelectorAll('.draw-colors li');
|
||||
const widths = document.querySelectorAll('.draw-widths li');
|
||||
const eraser = document.querySelector('.draw-eraser');
|
||||
|
||||
Draw.draw.classList.add('open');
|
||||
|
||||
let height, width;
|
||||
if (Draw.backgroundCanvas) {
|
||||
Draw.bgHeight = Draw.backgroundCanvas.height;
|
||||
Draw.bgWidth = Draw.backgroundCanvas.width;
|
||||
const dheight = document.body.clientHeight;
|
||||
const dwidth = document.body.clientWidth;
|
||||
if (Draw.bgHeight <= dheight && Draw.bgWidth <= dwidth) {
|
||||
height = Draw.bgHeight;
|
||||
width = Draw.bgWidth;
|
||||
} else {
|
||||
const bgTaller = Draw.bgHeight / Draw.bgWidth > dheight / dwidth;
|
||||
if (Draw.bgHeight <= dheight || !bgTaller) {
|
||||
width = dwidth;
|
||||
height = dwidth * Draw.bgHeight / Draw.bgWidth;
|
||||
Draw.ratio = Draw.bgWidth / dwidth;
|
||||
} else if (Draw.bgWidth <= dwidth || bgTaller) {
|
||||
height = dheight;
|
||||
width = dheight * Draw.bgWidth / Draw.bgHeight;
|
||||
Draw.ratio = Draw.bgHeight / dheight;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
height = document.body.clientHeight;
|
||||
width = document.body.clientWidth;
|
||||
}
|
||||
|
||||
canvasWrapper.style.height = `${height}px`;
|
||||
canvasWrapper.style.width = `${width}px`;
|
||||
|
||||
Draw.canvas = document.getElementById('draw-canvas');
|
||||
Draw.canvas.width = width;
|
||||
Draw.canvas.height = height;
|
||||
Draw.ctx = Draw.canvas.getContext('2d');
|
||||
Draw.ctx.lineCap = 'round';
|
||||
|
||||
Draw.screen = document.getElementById('screen-canvas');
|
||||
Draw.screen.width = width;
|
||||
Draw.screen.height = height;
|
||||
Draw.sctx = Draw.screen.getContext('2d');
|
||||
Draw.sctx.lineCap = Draw.ctx.lineCap;
|
||||
|
||||
Draw.bg = document.getElementById('draw-background');
|
||||
Draw.bg.width = width;
|
||||
Draw.bg.height = height;
|
||||
bgctx = Draw.bg.getContext('2d');
|
||||
|
||||
if (Draw.backgroundCanvas) {
|
||||
// copy over background image
|
||||
bgctx.drawImage(Draw.backgroundCanvas, 0, 0, width, height);
|
||||
} else {
|
||||
// fill canvas with white
|
||||
bgctx.fillStyle = 'white';
|
||||
bgctx.fillRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
// init controls
|
||||
Draw.ctx.strokeStyle = '#000';
|
||||
Draw.sctx.strokeStyle = '#000';
|
||||
colors.forEach(item => item.classList.remove('selected'));
|
||||
eraser.classList.remove('selected');
|
||||
document.querySelector('[data-color=black]').classList.add('selected');
|
||||
Draw.ctx.lineWidth = Draw.MEDIUM;
|
||||
Draw.sctx.lineWidth = Draw.MEDIUM;
|
||||
widths.forEach(item => item.classList.remove('selected'));
|
||||
document.querySelector('[data-width=medium]').classList.add('selected');
|
||||
|
||||
if (Draw.draw.classList.contains('bound')) return;
|
||||
|
||||
// Get a regular interval for drawing to the screen
|
||||
window.requestAnimFrame = (function (callback) {
|
||||
return window.requestAnimationFrame ||
|
||||
window.webkitRequestAnimationFrame ||
|
||||
window.mozRequestAnimationFrame ||
|
||||
window.oRequestAnimationFrame ||
|
||||
window.msRequestAnimaitonFrame ||
|
||||
function (callback) {
|
||||
window.setTimeout(callback, 1000 / 60);
|
||||
};
|
||||
})();
|
||||
// Allow for animation
|
||||
(function drawLoop() {
|
||||
requestAnimFrame(drawLoop);
|
||||
Draw.renderCanvas();
|
||||
})();
|
||||
|
||||
Draw.screen.addEventListener('mousedown', Draw.startDrawing, true);
|
||||
Draw.screen.addEventListener('mouseenter', Draw.startDrawing, false);
|
||||
Draw.screen.addEventListener('mouseup', Draw.stopDrawing, false);
|
||||
Draw.screen.addEventListener('mouseleave', Draw.stopDrawing, false);
|
||||
Draw.screen.addEventListener('mousemove', function (e) {
|
||||
Draw.mousePos = Draw.getPos(Draw.screen, e);
|
||||
}, false);
|
||||
|
||||
// Set up touch events for mobile, etc
|
||||
Draw.screen.addEventListener('touchstart', function (e) {
|
||||
Draw.mousePos = Draw.getPos(Draw.screen, e);
|
||||
var touch = e.touches[0];
|
||||
var mouseEvent = new MouseEvent('mousedown', {
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY,
|
||||
buttons: 1
|
||||
});
|
||||
Draw.screen.dispatchEvent(mouseEvent);
|
||||
}, false);
|
||||
Draw.screen.addEventListener('touchend', Draw.stopDrawing, false);
|
||||
Draw.screen.addEventListener('touchmove', function (e) {
|
||||
var touch = e.touches[0];
|
||||
var mouseEvent = new MouseEvent('mousemove', {
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY
|
||||
});
|
||||
Draw.screen.dispatchEvent(mouseEvent);
|
||||
}, false);
|
||||
|
||||
// Clear canvas
|
||||
const clear = document.getElementById('draw-clear');
|
||||
clear.addEventListener('click', (e) => {
|
||||
const rect = Draw.screen.getBoundingClientRect();
|
||||
Draw.ctx.clearRect(0, 0, rect.width, rect.height);
|
||||
Draw.drawingData = [];
|
||||
}, false);
|
||||
|
||||
// Save (background +) drawing
|
||||
Draw.save = document.querySelector('#draw-save');
|
||||
Draw.save.onclick = (e) => {
|
||||
const finalCanvas = document.createElement('canvas');
|
||||
let crop, height, width, bg;
|
||||
|
||||
if (Draw.backgroundCanvas) {
|
||||
width = Draw.bgWidth;
|
||||
height = Draw.bgHeight;
|
||||
bg = Draw.backgroundCanvas;
|
||||
} else {
|
||||
crop = Draw.autoCropDrawing();
|
||||
width = crop.width;
|
||||
height = crop.height;
|
||||
bg = Draw.bg;
|
||||
}
|
||||
finalCanvas.setAttribute('width', width);
|
||||
finalCanvas.setAttribute('height', height);
|
||||
|
||||
const finalctx = finalCanvas.getContext('2d');
|
||||
finalctx.lineCap = 'round';
|
||||
|
||||
if (Draw.backgroundCanvas) {
|
||||
Draw.upscaleDrawing(finalctx);
|
||||
} else {
|
||||
finalctx.putImageData(crop, 0, 0);
|
||||
}
|
||||
|
||||
// add background
|
||||
finalctx.globalCompositeOperation = 'destination-over';
|
||||
finalctx.drawImage(
|
||||
bg,
|
||||
0, 0,
|
||||
width,
|
||||
height
|
||||
);
|
||||
|
||||
// Disable button
|
||||
Draw.save.classList.add('disabled');
|
||||
|
||||
finalCanvas.toBlob(
|
||||
function (blob) {
|
||||
Upload.prepare(blob);
|
||||
Upload.name = 'drawing.jpg';
|
||||
Upload.init();
|
||||
},
|
||||
'image/jpeg',
|
||||
0.85
|
||||
);
|
||||
};
|
||||
|
||||
// Use the eraser
|
||||
eraser.addEventListener('click', function (e) {
|
||||
colors.forEach(item => item.classList.remove('selected'));
|
||||
this.classList.add('selected');
|
||||
|
||||
Draw.ctx.globalCompositeOperation = 'destination-out';
|
||||
}, false);
|
||||
|
||||
// Change pencil color
|
||||
for (let i = 0; i < colors.length; i++) {
|
||||
colors[i].addEventListener('click', function (e) {
|
||||
colors.forEach(item => item.classList.remove('selected'));
|
||||
eraser.classList.remove('selected');
|
||||
this.classList.add('selected');
|
||||
|
||||
Draw.ctx.globalCompositeOperation = 'source-over';
|
||||
Draw.ctx.strokeStyle = window.getComputedStyle(colors[i].querySelector('span.primary')).backgroundColor;
|
||||
Draw.sctx.strokeStyle = window.getComputedStyle(colors[i].querySelector('span.primary')).backgroundColor;
|
||||
});
|
||||
}
|
||||
|
||||
// Change pencil thickness
|
||||
for (let i = 0; i < widths.length; i++) {
|
||||
widths[i].addEventListener('click', function (e) {
|
||||
widths.forEach(item => item.classList.remove('selected'));
|
||||
this.classList.add('selected');
|
||||
|
||||
let width;
|
||||
switch (this.dataset.width) {
|
||||
case 'small':
|
||||
width = Draw.SMALL;
|
||||
break;
|
||||
case 'medium':
|
||||
width = Draw.MEDIUM;
|
||||
break;
|
||||
case 'big':
|
||||
width = Draw.BIG;
|
||||
break;
|
||||
default:
|
||||
width = Draw.SMALL;
|
||||
}
|
||||
Draw.ctx.lineWidth = width;
|
||||
Draw.sctx.lineWidth = width;
|
||||
});
|
||||
}
|
||||
|
||||
const drawback = document.querySelector('#draw #drawback');
|
||||
drawback.addEventListener('click', () => {
|
||||
history.back();
|
||||
});
|
||||
|
||||
// Add a flag to not re-bind event listeners
|
||||
Draw.draw.classList.add('bound');
|
||||
},
|
||||
|
||||
stopDrawing: function (e) {
|
||||
// print to actual canvas
|
||||
if (Draw.drawingData.length)
|
||||
Draw.smoothenLine(Draw.drawingData[Draw.drawingData.length - 1], Draw.ctx, 1)
|
||||
// cleanup screen
|
||||
const rect = Draw.screen.getBoundingClientRect();
|
||||
Draw.sctx.clearRect(0, 0, rect.width, rect.height);
|
||||
|
||||
Draw.drawing = false;
|
||||
Draw.lastPos = null;
|
||||
Draw.mousePos = null;
|
||||
|
||||
// show coontrols
|
||||
Draw.topNav.classList.remove('drawing');
|
||||
Draw.actions.classList.remove('drawing');
|
||||
Draw.control.classList.remove('drawing');
|
||||
|
||||
Draw.ctx.beginPath();
|
||||
Draw.sctx.beginPath();
|
||||
},
|
||||
|
||||
startDrawing: function (e) {
|
||||
if (e.buttons == 1) {
|
||||
Draw.drawing = true;
|
||||
|
||||
// hide controls
|
||||
Draw.topNav.classList.add('drawing');
|
||||
Draw.actions.classList.add('drawing');
|
||||
Draw.control.classList.add('drawing');
|
||||
|
||||
// save drawing data
|
||||
const data = {
|
||||
gco: Draw.sctx.globalCompositeOperation,
|
||||
width: Draw.sctx.lineWidth,
|
||||
color: Draw.sctx.strokeStyle,
|
||||
points: []
|
||||
}
|
||||
Draw.drawingData.push(data);
|
||||
|
||||
Draw.lastPos = Draw.getPos(Draw.screen, e);
|
||||
}
|
||||
},
|
||||
|
||||
upscaleDrawing: function (finalctx) {
|
||||
// re-draw upscaled
|
||||
for (let i = 0; i < Draw.drawingData.length; i++) {
|
||||
finalctx.globalCompositeOperation = Draw.drawingData[i].gco;
|
||||
finalctx.lineWidth = Draw.drawingData[i].width * Draw.ratio;
|
||||
finalctx.strokeStyle = Draw.drawingData[i].color;
|
||||
|
||||
Draw.smoothenLine(Draw.drawingData[i], finalctx, Draw.ratio);
|
||||
finalctx.beginPath();
|
||||
}
|
||||
},
|
||||
|
||||
smoothenLine(line, ctx, ratio) {
|
||||
let j = 0;
|
||||
// keep only half of the points
|
||||
const sample = line.points.filter((point, index) => index % 2 == 0)
|
||||
|
||||
while (sample.length < 4) {
|
||||
sample.push(sample[sample.length - 1]);
|
||||
}
|
||||
|
||||
if (sample.length >= 4) {
|
||||
// move to 1st point of the line
|
||||
ctx.moveTo(
|
||||
sample[j].x * ratio,
|
||||
sample[j].y * ratio
|
||||
);
|
||||
for (j = 1; j < sample.length - 2; j++) {
|
||||
// define controle point as the mean point between current point and next point
|
||||
const c = (sample[j].x + sample[j + 1].x) / 2;
|
||||
const d = (sample[j].y + sample[j + 1].y) / 2;
|
||||
|
||||
// draw a curve between
|
||||
ctx.quadraticCurveTo(
|
||||
sample[j].x * ratio,
|
||||
sample[j].y * ratio,
|
||||
c * ratio,
|
||||
d * ratio
|
||||
);
|
||||
}
|
||||
ctx.quadraticCurveTo(
|
||||
sample[j].x * ratio,
|
||||
sample[j].y * ratio,
|
||||
sample[j + 1].x * ratio,
|
||||
sample[j + 1].y * ratio
|
||||
);
|
||||
}
|
||||
ctx.stroke();
|
||||
},
|
||||
|
||||
autoCropDrawing: function () {
|
||||
const rect = Draw.screen.getBoundingClientRect();
|
||||
let height = rect.height;
|
||||
let width = rect.width;
|
||||
|
||||
const imgData = Draw.ctx.getImageData(0, 0, width, height);
|
||||
let maxx = 0, minx = width, maxy = 0, miny = height,
|
||||
index;
|
||||
|
||||
// list all non transparent pixels
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
index = (y * width + x) * 4;
|
||||
if (imgData.data[index + 3] > 0) {
|
||||
minx = Math.min(x, minx);
|
||||
miny = Math.min(y, miny);
|
||||
maxx = Math.max(x, maxx);
|
||||
maxy = Math.max(y, maxy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
width = maxx - minx + 1;
|
||||
height = maxy - miny + 1;
|
||||
|
||||
return Draw.ctx.getImageData(
|
||||
minx - (width * Draw.CROP_PADDING),
|
||||
miny - (height * Draw.CROP_PADDING),
|
||||
width * (1 + 2 * Draw.CROP_PADDING),
|
||||
height * (1 + 2 * Draw.CROP_PADDING)
|
||||
)
|
||||
},
|
||||
|
||||
// Get the position of the mouse/touch relative to the canvas
|
||||
getPos: function (canvasDom, event) {
|
||||
var rect = canvasDom.getBoundingClientRect();
|
||||
let x, y;
|
||||
if (event.touches) {
|
||||
x = event.touches[0].clientX - rect.left;
|
||||
y = event.touches[0].clientY - rect.top;
|
||||
} else {
|
||||
x = event.clientX - rect.left;
|
||||
y = event.clientY - rect.top;
|
||||
}
|
||||
|
||||
if (Draw.drawing) {
|
||||
let points = Draw.drawingData[Draw.drawingData.length - 1].points;
|
||||
points.push({ x, y });
|
||||
}
|
||||
|
||||
return { x, y };
|
||||
},
|
||||
|
||||
// Draw to the canvas
|
||||
renderCanvas: function () {
|
||||
if (Draw.drawing && Draw.lastPos && Draw.mousePos) {
|
||||
Draw.sctx.moveTo(Draw.lastPos.x, Draw.lastPos.y);
|
||||
Draw.sctx.lineTo(Draw.mousePos.x, Draw.mousePos.y);
|
||||
Draw.sctx.stroke();
|
||||
Draw.lastPos = Draw.mousePos;
|
||||
}
|
||||
},
|
||||
|
||||
disableForCanvas: function (e) {
|
||||
if (e.target.tagName == 'canvas') {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
};
|
||||
MovimEvents.registerWindow('loaded', 'draw', () => {
|
||||
Upload.attach((file) => {
|
||||
if (Draw.draw) Draw.draw.classList = '';
|
||||
if (Draw.save) {
|
||||
Draw.save.classList.remove('disabled');
|
||||
Draw.save.style.backgroundImage = '';
|
||||
}
|
||||
});
|
||||
|
||||
Upload.fail(() => {
|
||||
if (Draw.draw) Draw.draw.classList = 'upload';
|
||||
if (Draw.save) {
|
||||
Draw.save.classList.remove('disabled');
|
||||
Draw.save.style.backgroundImage = '';
|
||||
}
|
||||
});
|
||||
|
||||
Upload.progress((percent) => {
|
||||
if (Draw.save) {
|
||||
Draw.save.style.backgroundImage
|
||||
= 'linear-gradient(to top, rgba(0, 0, 0, 0.5) ' + percent + '%, transparent ' + percent + '%)';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
MovimEvents.registerBody('touchstart', 'draw', (e) => Draw.disableForCanvas(e));
|
||||
MovimEvents.registerBody('touchend', 'draw', (e) => Draw.disableForCanvas(e));
|
||||
MovimEvents.registerBody('touchmove', 'draw', (e) => Draw.disableForCanvas(e));
|
||||
@@ -0,0 +1,2 @@
|
||||
[draw]
|
||||
title = Draw
|
||||
@@ -0,0 +1,67 @@
|
||||
.drawer {
|
||||
position: fixed;
|
||||
z-index: 3;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
max-height: calc(100% - 7rem);
|
||||
box-shadow: var(--elevation-5);
|
||||
transition: transform .45s var(--decelerate-easing);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 2rem 2rem 0 0;
|
||||
}
|
||||
|
||||
.drawer.tiny {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.drawer>section {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.drawer>section form>div:not(:has(ul.list)) {
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
|
||||
.drawer:empty {
|
||||
transition: transform 0.45s var(--accelerate-easing);
|
||||
transform: translateY(100%);
|
||||
pointer-events: none;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.drawer ul.list li.subheader.sticky {
|
||||
background-color: rgb(var(--movim-background));
|
||||
}
|
||||
|
||||
@media screen and (min-width: 570px) {
|
||||
.drawer {
|
||||
width: 70rem;
|
||||
left: calc(50% - 35rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 570px) {
|
||||
.drawer~main, .drawer~nav {
|
||||
transition: transform .25s var(--decelerate-easing);
|
||||
}
|
||||
|
||||
.drawer:not(:empty)~main,
|
||||
.drawer:not(:empty)~nav {
|
||||
transition: transform .25s var(--accelerate-easing);
|
||||
transform: translateY(-2rem);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#drawer ul.list>li.search form div:after {
|
||||
background-color: rgb(var(--movim-background-main));
|
||||
}
|
||||
|
||||
#drawer ul.list>li.search form div:focus-within:after {
|
||||
background-color: rgba(var(--movim-background-main), 0.8);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
var Drawer = {
|
||||
open: function (key) {
|
||||
document.querySelector('#drawer').dataset.type = key;
|
||||
MovimTpl.pushAnchorState(key, function () { Drawer.clear(key) });
|
||||
},
|
||||
filled: function () {
|
||||
return (document.querySelector('#drawer').innerHTML != '');
|
||||
},
|
||||
hasTabs: function () {
|
||||
return Drawer.filled()
|
||||
&& document.querySelector('#drawer').contains(document.querySelector('#navtabs'));
|
||||
},
|
||||
clear: function (key) {
|
||||
if (key && document.querySelector('#drawer').dataset.type == key) {
|
||||
Drawer_ajaxClear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (key == undefined) {
|
||||
MovimTpl.clearAnchorState();
|
||||
Drawer_ajaxClear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof MovimWebsocket != 'undefined') {
|
||||
MovimWebsocket.initiate(() => document.querySelector('#drawer').innerHTML = '');
|
||||
}
|
||||
|
||||
MovimEvents.registerBody('click', 'drawer', (e) => {
|
||||
if (Drawer.filled()
|
||||
&& document.querySelector('body') == e.target) {
|
||||
e.stopPropagation();
|
||||
history.back();
|
||||
}
|
||||
});
|
||||
|
||||
MovimEvents.registerBody('keydown', 'drawer', (e) => {
|
||||
if (Drawer.filled()
|
||||
&& e.key == 'Escape') {
|
||||
history.back();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
#emojisconfig_widget .pack {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
#emojisconfig_widget .pack:not(:last-child) {
|
||||
border-bottom: 1px solid rgba(var(--movim-font), 0.12);
|
||||
}
|
||||
|
||||
#emojisconfig_widget .pack .emojis {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#emojisconfig_widget .pack .emojis img {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
#emojisconfig_widget .pack .emojis img:hover {
|
||||
cursor: pointer;
|
||||
background-color: rgba(var(--movim-font), 0.05);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
#emojisconfig_widget .pack .emojis img.favorite {
|
||||
opacity: 0.05;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
MovimWebsocket.attach(function () {
|
||||
EmojisConfig_ajaxHttpGet();
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
[emojisconfig]
|
||||
title = Emojis
|
||||
favorites_title = My favorite emojis
|
||||
favorites_text = Pick emojis from the packs bellow that you want to add to your favorites ones
|
||||
dialog_title = Manage a favorite emoji
|
||||
alias_error = The shortcut should only contains lowercase letters, numbers and dashes
|
||||
new_added = New favorite emoji added
|
||||
alias_conflict = The shortcut is already used on another emoji
|
||||
no_emojis_yet = No favorites yet, check the settings page to add some
|
||||
@@ -0,0 +1,22 @@
|
||||
[faq]
|
||||
title = Frequently Asked Questions
|
||||
permission_title = What does the permission setting do?
|
||||
permission_text = By default the content is only available to your contacts. You can make your profile and publications public by enabling the related setting.
|
||||
permission_community_title = How does it apply to Communities posts?
|
||||
permission_community_text = Similary to your blog publications except that, by default, the posts are only available to the members or subscribers of the Community (except if you explicitly open all the Community content)
|
||||
chatrooms_title = Where can I find a list of group chats?
|
||||
find_contacts_title = Where can I find my contacts?
|
||||
find_contacts_text = By opening the search panel.
|
||||
|
||||
[apps]
|
||||
title = Applications
|
||||
install = Movim App
|
||||
install_text = Install Movim on your device
|
||||
recommend = We also recommend
|
||||
|
||||
[contact]
|
||||
title = Administrators & Contact
|
||||
|
||||
[chatroom]
|
||||
question = Chat with the Movim team?
|
||||
button = Add the chatroom
|
||||
@@ -0,0 +1,42 @@
|
||||
[error]
|
||||
username = Wrong username
|
||||
jid = Invalid XMPP Address
|
||||
empty_challenge = Empty Challenge from the server
|
||||
dns = XMPP Domain error, your account is not a valid XMPP Address
|
||||
data_missings = Some data are missing!
|
||||
internal = Internal server error
|
||||
session = Session error
|
||||
account_created = Account successfully created
|
||||
connection = Movim cannot connect to the server
|
||||
impossible = Impossible login
|
||||
|
||||
default = Unknown error
|
||||
login_format = Invalid username format
|
||||
password_format = Invalid password format
|
||||
unauthorized = Your XMPP server is unauthorized
|
||||
wrong_account = Movim failed to authenticate. You entered wrong data
|
||||
mechanism = Authentication mechanism not supported by Movim
|
||||
fail_auth = The XMPP authentification failed
|
||||
max_sessions_reached = This Movim instance is currently full.
|
||||
max_sessions_reached_join = Try a different one on %s
|
||||
|
||||
[connection]
|
||||
socket_connected = Connected to the server
|
||||
authenticated = Authenticated to the server
|
||||
|
||||
[form]
|
||||
username = My address
|
||||
password = Password
|
||||
|
||||
whitelist_info = Login with accounts from these servers
|
||||
connect_info = Login using any XMPP account or create a new one
|
||||
connected = Connected
|
||||
population = Population
|
||||
|
||||
invite_chatroom = %s invited you to join a chatroom
|
||||
|
||||
pod_admins = Administrated by
|
||||
|
||||
[about_movim]
|
||||
info = Movim is a federated blogging and chat platform.
|
||||
website = Visit the project official website %s.
|
||||
@@ -0,0 +1,84 @@
|
||||
body main {
|
||||
background: radial-gradient(circle farthest-corner at right bottom, rgb(255, 61, 0) 0, rgb(61, 58, 244) 90%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#login_widget {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100rem;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
body main {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
#login_widget {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#login_widget:before {
|
||||
content: '';
|
||||
display: block;
|
||||
background: url(../theme/img/app/logo.svg);
|
||||
background-size: auto 4rem, 100% 100%;
|
||||
background-position: center 2rem, center;
|
||||
background-repeat: no-repeat;
|
||||
height: 8rem;
|
||||
}
|
||||
}
|
||||
|
||||
#login_widget .dialog {
|
||||
flex: 0 1 50%;
|
||||
position: relative;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#login_widget .dialog>section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#login_widget article>section {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#login_widget .dialog>section .about {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
#login_widget .dialog .banner {
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
margin-top: -1rem;
|
||||
object-fit: cover;
|
||||
min-height: 9rem;
|
||||
max-height: 18rem;
|
||||
width: 100%;
|
||||
mask-image: linear-gradient(to bottom, black 60%, transparent);
|
||||
}
|
||||
|
||||
#login_widget:not(.error)>#error,
|
||||
#login_widget.error>div:not(#error) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#login_widget input#complete {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: -1;
|
||||
color: rgba(var(--movim-font), 0.54);
|
||||
box-shadow: none;
|
||||
width: calc(100% - 3rem);
|
||||
}
|
||||
|
||||
#login_error code {
|
||||
display: block;
|
||||
margin-top: 1rem;
|
||||
max-height: 10rem;
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
var Login = {
|
||||
domain: '@movim.eu',
|
||||
submitted: false,
|
||||
|
||||
init: function () {
|
||||
// The form submission event
|
||||
document.body.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
Login.submitted = true;
|
||||
|
||||
// We register the socket
|
||||
MovimWebsocket.connection.register(this.querySelector('input#username').value.replace(/.*@/, ''));
|
||||
var button = this.querySelector('input[type=submit]');
|
||||
button.value = button.dataset.loading;
|
||||
|
||||
localStorage.username = document.querySelector('input#username').value;
|
||||
|
||||
// A fallback security
|
||||
setTimeout('MovimWebsocket.unregister()', 20000);
|
||||
|
||||
return true;
|
||||
}, false);
|
||||
},
|
||||
|
||||
setCookie: function (value, expires) {
|
||||
document.cookie = 'MOVIM_SESSION_ID' + '=' + value + '; expires=' + expires + '; path=/ ; secure; SameSite=lax';
|
||||
},
|
||||
|
||||
getCookie: function () {
|
||||
cookie = document.cookie.split('=');
|
||||
|
||||
if (cookie[0] == 'MOVIM_SESSION_ID') {
|
||||
return cookie[1].substring(0, 32);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
setQuick: function (deviceId, login, host, key) {
|
||||
localStorage.setItem('quickDeviceId', deviceId);
|
||||
localStorage.setItem('quickLogin', login);
|
||||
localStorage.setItem('quickHost', host);
|
||||
localStorage.setItem('quickKey', key);
|
||||
},
|
||||
|
||||
clearQuick: function () {
|
||||
localStorage.removeItem('quickDeviceId');
|
||||
localStorage.removeItem('quickLogin');
|
||||
localStorage.removeItem('quickHost');
|
||||
localStorage.removeItem('quickKey');
|
||||
},
|
||||
|
||||
isQuick: function () {
|
||||
return localStorage.getItem('quickDeviceId') != null
|
||||
&& localStorage.getItem('quickLogin') != null
|
||||
&& localStorage.getItem('quickHost') != null
|
||||
&& localStorage.getItem('quickKey') != null;
|
||||
},
|
||||
|
||||
quickLogin: function () {
|
||||
if (Login.isQuick()) {
|
||||
Login_ajaxQuickLogin(
|
||||
localStorage.getItem('quickDeviceId'),
|
||||
localStorage.getItem('quickLogin'),
|
||||
localStorage.getItem('quickKey'),
|
||||
Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
Login.getCookie(),
|
||||
true, // check is we can actually quick login before registering
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
quickLoginRegister: function () {
|
||||
MovimWebsocket.connection.register(localStorage.getItem('quickHost'));
|
||||
}
|
||||
}
|
||||
|
||||
// The session linker is launched
|
||||
MovimWebsocket.start(function () {
|
||||
Login.quickLogin();
|
||||
Login.init();
|
||||
|
||||
// We enable the form
|
||||
var inputs = document.querySelectorAll('#login_widget div input[disabled]');
|
||||
for (var i = 0; i < inputs.length; i++) {
|
||||
inputs[i].disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
MovimWebsocket.register(function () {
|
||||
if (Login.isQuick()) {
|
||||
Login_ajaxQuickLogin(
|
||||
localStorage.getItem('quickDeviceId'),
|
||||
localStorage.getItem('quickLogin'),
|
||||
localStorage.getItem('quickKey'),
|
||||
Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
Login.getCookie()
|
||||
);
|
||||
} else {
|
||||
form = document.querySelector('form[name="login"]');
|
||||
if (Login.submitted && Login.getCookie()) {
|
||||
Login_ajaxLogin(MovimUtils.formToJson('login'), Login.getCookie(), Intl.DateTimeFormat().resolvedOptions().timeZone);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
MovimEvents.registerWindow('loaded', 'login', () => {
|
||||
// We had the autocomplete system
|
||||
var login = document.querySelector('input#username');
|
||||
|
||||
login.addEventListener('input', function () {
|
||||
if (this.value.indexOf('@') == -1) {
|
||||
document.querySelector('input#complete').value = this.value + '@' + Login.domain;
|
||||
} else {
|
||||
document.querySelector('input#complete').value = this.value;
|
||||
}
|
||||
|
||||
if (this.value.length == 0) {
|
||||
document.querySelector('input#complete').value = '';
|
||||
}
|
||||
});
|
||||
|
||||
login.addEventListener('blur', function () {
|
||||
this.value = document.querySelector('input#complete').value;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
[menu]
|
||||
empty_title = No news yet…
|
||||
empty = Welcome to your news feed, here you will see all the posts published by your contacts and in the communities you have subscribed.
|
||||
|
||||
all = All
|
||||
mine = My publications
|
||||
contacts = Contacts
|
||||
add_post = Write a new post…
|
||||
@@ -0,0 +1,23 @@
|
||||
#menu_widget {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
#menu_refresh {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1024px) {
|
||||
#menu_widget > div {
|
||||
padding: 0 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
#menu_refresh li.refreshing {
|
||||
background: linear-gradient(to right, rgb(var(--movim-background-main)) 8%, rgba(var(--movim-background), 0.5) 18%, rgb(var(--movim-background-main)) 33%);
|
||||
animation: 1.5s shine linear infinite;
|
||||
background-size: 200% 100%;
|
||||
}
|
||||
|
||||
#menu_refresh li.refreshing span.primary i {
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
var Menu = {
|
||||
init: function() {
|
||||
let parts = MovimUtils.urlParts();
|
||||
let page = parts.params[0] !== undefined ? parseInt(parts.params[0]) : 0;
|
||||
|
||||
if (parts.hash != '') {
|
||||
switch (parts.hash) {
|
||||
case 'communities':
|
||||
Menu_ajaxHttpGetCommunities(page);
|
||||
break;
|
||||
case 'contacts':
|
||||
Menu_ajaxHttpGetContacts(page);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
Menu_ajaxHttpGetAll(page);
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function() {
|
||||
document.querySelector('#menu_refresh li').classList.add('refreshing');
|
||||
Menu_ajaxHttpGetAll();
|
||||
},
|
||||
|
||||
setLoad: function(tab) {
|
||||
tab.innerHTML = '<i class="material-symbols spin">autorenew</i>';
|
||||
}
|
||||
};
|
||||
|
||||
MovimWebsocket.attach(function() {
|
||||
Menu.init();
|
||||
Notif_ajaxClear('news');
|
||||
Notif.current('news');
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
MovimWebsocket.initiate(() => Navigation_ajaxHttpRefresh());
|
||||
@@ -0,0 +1,8 @@
|
||||
MovimWebsocket.initiate(() => {
|
||||
if (!MovimUtils.isMobile()) {
|
||||
NewsNav_ajaxHttpGet(
|
||||
MovimUtils.urlParts().page,
|
||||
MovimUtils.urlParts().params[0]
|
||||
)
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
#notfound_widget {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
[notification]
|
||||
request_title = Notifications request
|
||||
request_info = Movim requires your approval to display notifications
|
||||
request_info2 = This authorization can be revoked at any time
|
||||
request_button = Request
|
||||
request_granted = Authorization granted
|
||||
request_refused = Authorization refused
|
||||
@@ -0,0 +1,282 @@
|
||||
var Notif = {
|
||||
inhibed: false,
|
||||
focused: false,
|
||||
call_status: null,
|
||||
tab_counter1: 0,
|
||||
tab_counter2: 0,
|
||||
tab_counter1_key: 'chat',
|
||||
tab_counter2_key: 'news',
|
||||
document_title_init: null,
|
||||
document_title: document.title,
|
||||
notifs_key: '',
|
||||
favicon: null,
|
||||
|
||||
audioCall: null,
|
||||
|
||||
incomingMessage: function () {
|
||||
if (NOTIFICATION_CHAT) {
|
||||
var tone = new Audio(BASE_URI + 'theme/audio/message.opus');
|
||||
tone.play();
|
||||
}
|
||||
},
|
||||
incomingCall: function () {
|
||||
if (NOTIFICATION_CALL) {
|
||||
Notif.audioCall = new Audio(BASE_URI + 'theme/audio/call.opus');
|
||||
Notif.audioCall.addEventListener('ended', function () {
|
||||
this.currentTime = 0;
|
||||
this.play();
|
||||
}, false);
|
||||
Notif.audioCall.play();
|
||||
}
|
||||
},
|
||||
userJoinedCall: function () {
|
||||
var tone = new Audio(BASE_URI + 'theme/audio/user_joined.opus');
|
||||
tone.play();
|
||||
},
|
||||
userLeftCall: function () {
|
||||
var tone = new Audio(BASE_URI + 'theme/audio/user_left.opus');
|
||||
tone.play();
|
||||
},
|
||||
incomingCallAnswer: function () {
|
||||
if (Notif.audioCall) {
|
||||
Notif.audioCall.pause();
|
||||
Notif.audioCall.currentTime = 0;
|
||||
}
|
||||
},
|
||||
inhibit: function (sec) {
|
||||
Notif.inhibed = true;
|
||||
|
||||
if (sec == null) sec = 5;
|
||||
|
||||
setTimeout(function () {
|
||||
Notif.inhibed = false;
|
||||
},
|
||||
sec * 1000);
|
||||
},
|
||||
refresh: function (keys) {
|
||||
var counters = document.querySelectorAll('.counter');
|
||||
for (i = 0; i < counters.length; i++) {
|
||||
var n = counters[i];
|
||||
if (n.dataset.key != null
|
||||
&& keys[n.dataset.key] != null) {
|
||||
if (keys[n.dataset.key] > 100) keys[n.dataset.key] = '+100';
|
||||
n.innerHTML = keys[n.dataset.key];
|
||||
}
|
||||
}
|
||||
|
||||
for (var key in keys) {
|
||||
var counter = keys[key];
|
||||
Notif.setTab(key, counter);
|
||||
}
|
||||
|
||||
Notif.displayTab();
|
||||
|
||||
if (typeof BottomNavigation == 'object') BottomNavigation.setChatNotification(keys)
|
||||
},
|
||||
counter: function (key, counter) {
|
||||
var counters = document.querySelectorAll('.counter');
|
||||
for (i = 0; i < counters.length; i++) {
|
||||
var n = counters[i];
|
||||
if (n.dataset.key != null
|
||||
&& n.dataset.key == key) {
|
||||
var htmlCounter = String(counter);
|
||||
if (counter > 100) htmlCounter = '+100';
|
||||
if (counter == 0) htmlCounter = '';
|
||||
n.innerHTML = htmlCounter;
|
||||
}
|
||||
}
|
||||
|
||||
Notif.setTab(key, counter);
|
||||
Notif.displayTab();
|
||||
|
||||
if (typeof BottomNavigation == 'object') BottomNavigation?.setChatNotification({ [key]: counter });
|
||||
},
|
||||
setTab: function (key, counter) {
|
||||
if (Notif.tab_counter1_key == key) {
|
||||
Notif.tab_counter1 = counter;
|
||||
}
|
||||
if (Notif.tab_counter2_key == key) {
|
||||
Notif.tab_counter2 = counter;
|
||||
}
|
||||
},
|
||||
setTitle: function (title) {
|
||||
Notif.document_title = title;
|
||||
Notif.displayTab();
|
||||
},
|
||||
setCallStatus: function (status) {
|
||||
Notif.call_status = status;
|
||||
Notif.displayTab();
|
||||
},
|
||||
displayTab: function () {
|
||||
document.title = (Notif.call_status != null)
|
||||
? Notif.call_status + ' | '
|
||||
: '';
|
||||
|
||||
if (Notif.tab_counter1 == 0 && Notif.tab_counter2 == 0) {
|
||||
MovimFavicon.counter(0, 0);
|
||||
document.title += Notif.document_title;
|
||||
|
||||
if (typeof window.electron !== 'undefined')
|
||||
window.electron.notification(false);
|
||||
|
||||
if (typeof window.rambox !== 'undefined')
|
||||
window.rambox.setUnreadCount(0);
|
||||
} else {
|
||||
document.title +=
|
||||
Notif.tab_counter1
|
||||
+ '∣'
|
||||
+ Notif.tab_counter2
|
||||
+ ' • '
|
||||
+ Notif.document_title;
|
||||
|
||||
MovimFavicon.counter(Notif.tab_counter1, Notif.tab_counter2);
|
||||
|
||||
if (typeof window.electron !== 'undefined')
|
||||
window.electron.notification(Notif.tab_counter1 + Notif.tab_counter2);
|
||||
|
||||
if (typeof window.rambox !== 'undefined')
|
||||
window.rambox.setUnreadCount(Notif.tab_counter1 + Notif.tab_counter2);
|
||||
}
|
||||
},
|
||||
current: function (key) {
|
||||
Notif.notifs_key = key;
|
||||
Notif_ajaxCurrent(Notif.notifs_key);
|
||||
},
|
||||
snackbar: function (html, time) {
|
||||
if (Notif.inhibed == true) return;
|
||||
|
||||
target = document.getElementById('snackbar');
|
||||
|
||||
if (target) {
|
||||
target.innerHTML = html;
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
Notif.snackbarClear();
|
||||
}, time * 1000);
|
||||
},
|
||||
snackbarClear: function () {
|
||||
target = document.getElementById('snackbar');
|
||||
target.innerHTML = '';
|
||||
},
|
||||
desktop: function (tag, timestamp, title, body, picture, actions, data, force) {
|
||||
if (!force && (Notif.inhibed == true
|
||||
|| Notif.focused
|
||||
|| typeof Notification === 'undefined')
|
||||
|| !window.Notification
|
||||
|| !Notification.requestPermission) return;
|
||||
|
||||
if (Notification.permission === 'granted') {
|
||||
Notif.checkPushSubscription();
|
||||
|
||||
try {
|
||||
options = {
|
||||
badge: '/theme/img/app/badge.png',
|
||||
tag: tag,
|
||||
timestamp: timestamp * 1000,
|
||||
body: body,
|
||||
icon: picture,
|
||||
actions: actions,
|
||||
data: data,
|
||||
vibrate: [100, 50, 100],
|
||||
};
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.getRegistration(SW_URI).then(registration => {
|
||||
if (registration) {
|
||||
registration.showNotification(title, options);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
var notification = new Notification(
|
||||
title,
|
||||
options
|
||||
);
|
||||
|
||||
notification.onclick = function () {
|
||||
window.location.href = data.url;
|
||||
Notif.snackbarClear();
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.name == 'TypeError')
|
||||
return false;
|
||||
}
|
||||
} else if (Notification.permission !== 'denied') {
|
||||
Notif_ajaxHttpRequest();
|
||||
}
|
||||
},
|
||||
clear: function (tag) {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.getRegistration(SW_URI).then((registration) => {
|
||||
if (!registration) return;
|
||||
registration.getNotifications({ tag: tag }).then((notifications) =>
|
||||
notifications.forEach(notification => notification.close())
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
request: function () {
|
||||
Notification.requestPermission().then(permission => {
|
||||
(permission == 'granted')
|
||||
? Notif_ajaxRequestGranted()
|
||||
: Notif_ajaxRequestDenied();
|
||||
});
|
||||
},
|
||||
focus: function () {
|
||||
if (Notif.focused == false) {
|
||||
Notif.focused = true;
|
||||
Notif.current(Notif.notifs_key);
|
||||
}
|
||||
},
|
||||
checkPushSubscription() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.getRegistration(SW_URI).then(registration => {
|
||||
if (!registration) return;
|
||||
|
||||
registration.pushManager.getSubscription().then(pushSubscription => {
|
||||
if (pushSubscription == null) {
|
||||
// Register the push notification subcription
|
||||
registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: VAPID_PUBLIC_KEY
|
||||
}).then(function (subscription) {
|
||||
Notif.registerPushSubscription(subscription);
|
||||
}).catch(function (e) {
|
||||
console.error('Unable to subscribe to push', e);
|
||||
});
|
||||
} else {
|
||||
Notif_ajaxHttpTouchPushSubscription(pushSubscription.endpoint);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
registerPushSubscription(subscription) {
|
||||
Notif_ajaxRegisterPushSubscrition(
|
||||
subscription.endpoint,
|
||||
MovimUtils.arrayBufferToBase64(subscription.getKey('auth')),
|
||||
MovimUtils.arrayBufferToBase64(subscription.getKey('p256dh')),
|
||||
window.navigator.userAgent
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Notif.document_title_init = document.title;
|
||||
|
||||
if (typeof MovimWebsocket != 'undefined') {
|
||||
MovimWebsocket.attach(function () {
|
||||
Notif.document_title = Notif.document_title_init;
|
||||
Notif.tab_counter1 = Notif.tab_counter2 = 0;
|
||||
Notif_ajaxGet();
|
||||
Notif.current(Notif.notifs_key);
|
||||
|
||||
MovimEvents.registerWindow('blur', 'notifs', () => {
|
||||
Notif.focused = false;
|
||||
Notif_ajaxCurrent('blurred');
|
||||
});
|
||||
|
||||
MovimEvents.registerWindow('focus', 'notifs', () => Notif.focus());
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
[notificationconfig]
|
||||
title = Notifications
|
||||
push_subscriptions = Push notifications
|
||||
push_subscriptions_text = Movim will send push notifications to the following registered devices
|
||||
push_enabled = Push notifications enabled for this device
|
||||
push_disabled = Push notifications disabled for this device
|
||||
unknown_browser = Unknown browser
|
||||
unknown_platform = Unknown platform
|
||||
@@ -0,0 +1,18 @@
|
||||
MovimWebsocket.attach(function() {
|
||||
if (Notification.permission !== 'granted') {
|
||||
NotificationConfig_ajaxHttpRequest();
|
||||
}
|
||||
|
||||
navigator.serviceWorker.getRegistration(SW_URI).then((registration) => {
|
||||
if (!registration) {
|
||||
NotificationConfig_ajaxHttpPushGetConfig();
|
||||
return;
|
||||
}
|
||||
|
||||
registration.pushManager.getSubscription().then((pushSubscription) => {
|
||||
NotificationConfig_ajaxHttpPushGetConfig(pushSubscription ? pushSubscription.endpoint : null);
|
||||
}, () => {
|
||||
NotificationConfig_ajaxHttpPushGetConfig();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
[invitations]
|
||||
received = Received Invitations
|
||||
adds_you = %s wants to add you
|
||||
no_new = No new invitations
|
||||
no_subscriptions = No pending subscriptions
|
||||
wants_to_talk = %s wants to talk with you
|
||||
|
||||
[notifs]
|
||||
title = Notifications
|
||||
empty = No new notifications
|
||||
@@ -0,0 +1,4 @@
|
||||
#notifications_subscriptions:not(:empty) + .placeholder,
|
||||
#notifications_invitations:not(:empty) + .placeholder {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
var Notifications = {
|
||||
setCounters : function(counter) {
|
||||
var counters = document.querySelectorAll('.counter.notifications:not([data-key])');
|
||||
for (i = 0; i < counters.length; i++) {
|
||||
counters[i].innerHTML = counter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MovimWebsocket.attach(function() {
|
||||
Notifications_ajaxSetCounter();
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
[onboarding]
|
||||
notifications_title = Notifications
|
||||
notifications_text = Movim can notify you when a new post is published or if you receive messages
|
||||
notifications_text_second = You can always disable them afterwards
|
||||
public_title = Public profile
|
||||
public_text = Make your profile public and allow other users to find you and make new contacts more easily
|
||||
public_text_second = You can always disable this in the configuration afterwards
|
||||
@@ -0,0 +1,15 @@
|
||||
var Onboarding = {
|
||||
check: function() {
|
||||
if (localStorage.getItem('onboardingPublic') === null) {
|
||||
Onboarding_ajaxAskPublic();
|
||||
}
|
||||
},
|
||||
|
||||
setPublic: function() {
|
||||
localStorage.setItem('onboardingPublic', true);
|
||||
}
|
||||
}
|
||||
|
||||
MovimWebsocket.attach(function() {
|
||||
Onboarding.check();
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
[post]
|
||||
publish_something = Publish something
|
||||
news_feed = News Feed
|
||||
placeholder = Discover and register to the groups you are interested in
|
||||
content = Content
|
||||
published = Post published
|
||||
deleted = Post deleted
|
||||
hot_text = Posts recently published in communities that you are not subscribed (yet)
|
||||
new_blog = New blog post
|
||||
repost = This is a re-post from %s
|
||||
see_profile = See %s profile
|
||||
|
||||
unknown_contact= Unknown contact
|
||||
|
||||
read_time_singular = %s minute
|
||||
read_time_plural = %s minutes
|
||||
|
||||
public_preview = Preview URL: %s
|
||||
public_yes = This post is public
|
||||
public_no = This post is private
|
||||
public_url = Public URL of this post
|
||||
|
||||
comments_disabled_title = Disable the comments
|
||||
comments_disabled_text = Nobody will be able to like or comment
|
||||
comments_disabled_yes = Comments disabled for this post
|
||||
comments_disabled_no = Comments enabled for this post
|
||||
|
||||
delete_title = Delete this post
|
||||
delete_text = You are going to delete this post, please confirm your action
|
||||
delete_comment = Are you sure that you want to delete this comment?
|
||||
|
||||
discover = Discover more posts on the communities page
|
||||
|
||||
more = Read more
|
||||
|
||||
comments_disabled = Comments disabled
|
||||
comment_published = Comment published
|
||||
comment_like_published = Liked
|
||||
comment_publish_error = The comment cannot be published
|
||||
comment_deleted = Comment deleted
|
||||
liked = Liked your publication
|
||||
commented = Commented
|
||||
|
||||
subscription_required = You must be subscribed to %s to see this publication
|
||||
|
||||
[hello]
|
||||
contact_post = Contact post
|
||||
share_title = Universal share button
|
||||
share_text = Bookmark or drag and drop the following button in your toolbar and use it on all the pages you want to share on Movim
|
||||
share_button = Share on Movim
|
||||
enter_title = Oh! Hello!
|
||||
enter_paragraph = It seems that you are new here! Welcome to Movim!
|
||||
menu_title = Check the Menu
|
||||
menu_paragraph = All the main Movim features are accessible there. Don’t hesitate to navigate through the pages to discover more!
|
||||
profile_title = Looks like you didn’t complete your profile yet
|
||||
profile_paragraph = You can go in the Configuration page to describe yourself and put a nice picture
|
||||
@@ -0,0 +1,7 @@
|
||||
#comments {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
#comments li.mine ~ li button#like {
|
||||
display: none;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user