This commit is contained in:
nnduc
2026-10-24 14:03:52 +07:00
commit 4fe046de58
4790 changed files with 311146 additions and 0 deletions
+11
View File
@@ -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
+71
View File
@@ -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();
});
+19
View File
@@ -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
+54
View File
@@ -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();
}
});
+10
View File
@@ -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
+72
View File
@@ -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
+15
View File
@@ -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;
}
+4
View File
@@ -0,0 +1,4 @@
MovimWebsocket.attach(function() {
Avatar_ajaxHttpGetCurrent();
Avatar_ajaxGetAvatar();
});
+13
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
#blocked_widget_list:not(:empty) ~ .placeholder {
display: none;
}
+3
View File
@@ -0,0 +1,3 @@
MovimWebsocket.attach(function() {
Blocked_ajaxGet();
});
+9
View File
@@ -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
+3
View File
@@ -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
+57
View File
@@ -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));
}
});
+5
View File
@@ -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
+518
View File
@@ -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');
}
};
+17
View File
@@ -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
+55
View File
@@ -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;
}
+150
View File
@@ -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());
+13
View File
@@ -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());
+8
View File
@@ -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.
+39
View File
@@ -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;
}
+20
View File
@@ -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();
});
+37
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
#confirm ul h2,
#confirm ul h3 {
text-align: center;
}
+4
View File
@@ -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]);
}
}
});
+65
View File
@@ -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
+35
View File
@@ -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);
}
+161
View File
@@ -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();
});
}
}
+4
View File
@@ -0,0 +1,4 @@
[dictaphone]
name = Dictaphone
push_to_record = Push to record
recording = Recording
+131
View File
@@ -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;
}
+475
View File
@@ -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));
+2
View File
@@ -0,0 +1,2 @@
[draw]
title = Draw
+67
View File
@@ -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);
}
+43
View File
@@ -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
+22
View File
@@ -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
+42
View File
@@ -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.
+84
View File
@@ -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;
}
+127
View File
@@ -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;
});
});
+8
View File
@@ -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…
+23
View File
@@ -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;
}
+34
View File
@@ -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());
+8
View File
@@ -0,0 +1,8 @@
MovimWebsocket.initiate(() => {
if (!MovimUtils.isMobile()) {
NewsNav_ajaxHttpGet(
MovimUtils.urlParts().page,
MovimUtils.urlParts().params[0]
)
}
});
+3
View File
@@ -0,0 +1,3 @@
#notfound_widget {
text-align: center;
}
+7
View File
@@ -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
+282
View File
@@ -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();
});
+7
View File
@@ -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
+15
View File
@@ -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();
});
+56
View File
@@ -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
+7
View File
@@ -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