Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Settings page with avatar image upload #62

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions design/layouts/private.handlebars
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<li><a href="{{rootUrl}}/private/notifications">🔔 <span class="label">Notifications</span></a><span class="badge" id="newNotificationsBadge" hidden></span></li>
<li><a href="{{rootUrl}}/private/dms">💬 <span class="label">Messages</span></a><span class="badge" id="newDMsBadge" hidden></span></li>
<li><a href="{{rootUrl}}/private/post">➕ <span class="label">Compose</span></a></li>
<li><a href="{{rootUrl}}/private/settings">⚙️ <span class="label">Settings</span></a></li>

</ul>
</nav>
Expand Down
4 changes: 3 additions & 1 deletion design/partials/composer.handlebars
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
<input type="text" id="cw" placeholder="content warning" />
<input id="inReplyTo" placeholder="in reply to" value="{{inReplyTo}}" hidden />
<input id="to" placeholder="to" value="{{to}}" hidden />
<input id="attachment" type="file" />
<input id="description" type="text" placeholder="alt text" />
<button id="submit" type="submit">Post</button>
</form>
</div>
Expand All @@ -27,4 +29,4 @@
input.focus();
input.selectionStart = input.selectionEnd = input.value.length;

</script>
</script>
52 changes: 52 additions & 0 deletions design/settings.handlebars
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<div class="settings">
<header>Settings</header>

<div class="profileHeader">
{{#if actor.image}}
<img src="{{actor.image.url}}">
<input id="headerupload" type="file" class="settingsbutton"/>
{{/if}}
</div>
<div class="profileToolbar">
<img src="{{actor.icon.url}}" class="avatar"></a>
<input id="avatarupload" type="file" class="settingsbutton" />
<div class="tools">
</div>
</div>
<div class="profileBody">
<div class="profile">
<a href="{{or actor.url actor.id}}" class="author">{{actor.name}}</a>
<span class="handle">{{getUsername actor.id}}</span>
<p>
<label for="summary">Summary</label>
<textarea rows="4" cols="80" id="summary">{{actor.summary}}</textarea>
</p>

<form onsubmit="return app.settings()" id="settings">
<button id="save" type="submit">Save</button>
<button onclick="reload();">Cancel</button>
</form>

{{#if actor.attachment}}
<dl>
{{#each actor.attachment}}
<dt>
{{this.name}}
</dt>
<dd>
{{{this.value}}}
</dd>
{{/each}}
</dl>
{{/if}}
</div>
</div>

<script>
function reload() {
window.location = '/private/settings';
return false;
}
</script>


5 changes: 3 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ app.use(bodyParser.json({
type: 'application/activity+json'
})); // support json encoded bodies
app.use(bodyParser.json({
type: 'application/json'
type: 'application/json',
limit: '32mb' // allow large bodies as attachments are base64 in JSON
})); // support json encoded bodies
app.use(cookieParser())

Expand Down Expand Up @@ -155,4 +156,4 @@ ensureAccount(USERNAME, DOMAIN).then((myaccount) => {
http.createServer(app).listen(app.get('port'), function () {
console.log('Express server listening on port ' + app.get('port'));
});
});
});
50 changes: 46 additions & 4 deletions lib/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ import {
createFileName,
getFileName,
boostsFile,
pathToDMs
pathToDMs,
writeMediaFile,
readMediaFile
} from './storage.js';
import {
getActivity,
Expand Down Expand Up @@ -118,6 +120,16 @@ export const acceptDM = (dm, inboxUser) => {

}

export const writeMedia = (attachment) => {
logger('write media', attachment.hash, attachment.type);
writeMediaFile(attachment.hash, attachment); // store the JSON, so we have the type and data in a single file
}

export const readMedia = (filename) => {
logger('read media', filename);
return readMediaFile(filename);
}

export const isMyPost = (activity) => {
return (activity.id.startsWith(`https://${DOMAIN}/m/`));
}
Expand Down Expand Up @@ -335,7 +347,7 @@ export const sendToFollowers = async (object) => {

}

export const createNote = async (body, cw, inReplyTo, toUser) => {
export const createNote = async (body, cw, inReplyTo, toUser, attachmentInfo) => {
const publicAddress = "https://www.w3.org/ns/activitystreams#Public";

let d = new Date();
Expand All @@ -350,6 +362,27 @@ export const createNote = async (body, cw, inReplyTo, toUser) => {
ActivityPub.actor.followers
];

let attachment;
if (attachmentInfo) {
attachment = [
{
type: 'Document',
mediaType: attachmentInfo.type.split('/')[0],
url: `https://${ DOMAIN }/media/${attachmentInfo.hash}.${attachmentInfo.type.split('/')[1]}`,
name: attachmentInfo.description,
focalPoint: attachmentInfo.focalPoint,
blurhash: attachmentInfo.blurhash,
width: attachmentInfo.width,
height: attachmentInfo.height
}
];
if (attachmentInfo.width) {
attachment.width = attachmentInfo.width;
}
if (attachmentInfo.height) {
attachment.height = attachmentInfo.height;
}
}

// Contains mentions
const tags = [];
Expand Down Expand Up @@ -449,11 +482,11 @@ export const createNote = async (body, cw, inReplyTo, toUser) => {
"to": to,
"cc": cc,
directMessage,
"sensitive": cw !== null ? true : false,
"sensitive": cw ? true : false,
"atomUri": activityId,
"inReplyToAtomUri": null,
"content": content,
"attachment": [],
"attachment": attachment || [],
"tag": tags,
"replies": {
"id": `${activityId}/replies`,
Expand Down Expand Up @@ -610,3 +643,12 @@ export const ensureAccount = async (name, domain) => {
export const getAccount = () => {
return readJSONDictionary(accountFile, {});
}

export const updateAccountActor = (data) => {
let account = readJSONDictionary(accountFile, {});
Object.keys(data).forEach((k) => {
account.actor[k] = data[k];
});
writeJSONDictionary(accountFile, account);
}

30 changes: 28 additions & 2 deletions lib/storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const pathToFiles = path.resolve(dataDir, 'activitystream/');
export const pathToPosts = path.resolve(dataDir, 'posts/');
export const pathToUsers = path.resolve(dataDir, 'users/');
export const pathToDMs = path.resolve(dataDir, 'dms/');
export const pathToMedia = path.resolve(dataDir, 'media/');

export const followersFile = path.resolve(dataDir, 'followers.json');
export const followingFile = path.resolve(dataDir, 'following.json');
Expand Down Expand Up @@ -199,7 +200,12 @@ const ensureDataFolder = () => {
recursive: true
});
}

if (!fs.existsSync(path.resolve(pathToMedia))) {
logger('mkdir', pathToMedia);
fs.mkdirSync(path.resolve(pathToMedia), {
recursive: true
});
}
}


Expand All @@ -225,6 +231,26 @@ export const readJSONDictionary = (path, defaultVal = []) => {
}
}

// data is JSON stringified string containing {type: mimetype, data: base64}
export const writeMediaFile = (filename, attachment) => {
logger('write media', filename);
// write just the data part to file called <hash>
fs.writeFileSync(path.join(pathToMedia, filename + '.' + attachment.type.split('/')[1]), attachment.data);
delete attachment.data;
// write the remaining metadata to <hash>.json
fs.writeFileSync(path.join(pathToMedia, filename) + '.json', JSON.stringify(attachment));
}

// returns JSON stringified string containing {type: mimetype, data: base64}
export const readMediaFile = (filename) => {
logger('read media', filename);
// remove any .{ext} from filename
let bareFilename = filename.replace(/\..*/, '');
let attachment = JSON.parse(fs.readFileSync(path.join(pathToMedia, bareFilename) + '.json'));
attachment.data = fs.readFileSync(path.join(pathToMedia, bareFilename + '.' + attachment.type.split('/')[1]));
return attachment;
}

export const writeJSONDictionary = (path, data) => {
const now = new Date().getTime();
logger('write cache', path);
Expand All @@ -240,4 +266,4 @@ logger('BUILDING INDEX');
ensureDataFolder();
buildIndex().then(() => {
logger('INDEX BUILT!');
});
});
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
"md5": "^2.3.0",
"moment": "^2.29.4",
"node-fetch": "^3.3.0",
"rss-generator": "^0.0.3"
"rss-generator": "^0.0.3",
"blurhash": "^2.0.4",
"@andreekeberg/imagedata": "^1.0.2"
},
"keywords": [
"fediverse",
Expand Down
119 changes: 91 additions & 28 deletions public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,45 +162,108 @@ const app = {
}
return false;
},
settings: () => {
const summary = document.getElementById('summary');
let attachment_header;
let attachment_avatar;

app.readAttachment('avatarupload').then((att) => {
attachment_avatar = att;
return app.readAttachment('headerupload').then((att) => {
attachment_header = att;

const Http = new XMLHttpRequest();
const proxyUrl ='/private/settings';
Http.open("POST", proxyUrl);
Http.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
Http.send(JSON.stringify({
attachment_avatar: attachment_avatar,
attachment_header: attachment_header,
account: {
actor: {
summary: summary.value
}
}
}));

Http.onreadystatechange = () => {
if (Http.readyState == 4 && Http.status == 200) {
console.log('posted!');
window.location = '/private/settings';
} else {
console.error('HTTP PROXY CHANGE', Http);
}
}
});
});
return false;
},
readAttachment: async (id) => {
// read the file into base64, return mimetype and data
const files = document.getElementById(id).files;
return new Promise((resolve, reject) => {
if (files && files[0]) {
let f = files[0]; // only read the first file
let reader = new FileReader();
reader.onload = (function(theFile) {
return function(e) {
let base64 = btoa(
new Uint8Array(e.target.result)
.reduce((data, byte) => data + String.fromCharCode(byte), '')
);
resolve({type: f.type, data: base64});
};
})(f);
reader.readAsArrayBuffer(f);
} else {
resolve(null);
}
});
},
post: () => {
const post = document.getElementById('post');
const cw = document.getElementById('cw');
const inReplyTo = document.getElementById('inReplyTo');
const to = document.getElementById('to');
const description = document.getElementById('description');

const Http = new XMLHttpRequest();
const proxyUrl ='/private/post';
Http.open("POST", proxyUrl);
Http.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
Http.send(JSON.stringify({
post: post.value,
cw: cw.value,
inReplyTo: inReplyTo.value,
to: to.value,
}));
app.readAttachment('attachment').then((attachment) => {
const Http = new XMLHttpRequest();
const proxyUrl ='/private/post';
Http.open("POST", proxyUrl);
Http.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
Http.send(JSON.stringify({
post: post.value,
cw: cw.value,
inReplyTo: inReplyTo.value,
to: to.value,
attachment: attachment,
description: description.value
}));

Http.onreadystatechange = () => {
if (Http.readyState == 4 && Http.status == 200) {
console.log('posted!');
Http.onreadystatechange = () => {
if (Http.readyState == 4 && Http.status == 200) {
console.log('posted!');

// prepend the new post
const newHtml = Http.responseText;
const el = document.getElementById('home_stream') || document.getElementById('inbox_stream');
// prepend the new post
const newHtml = Http.responseText;
const el = document.getElementById('home_stream') || document.getElementById('inbox_stream');

if (!el) {
window.location = '/private/';
}
if (!el) {
window.location = '/private/';
}

// todo: ideally this would come back with all the html it needs
el.innerHTML = newHtml + el.innerHTML;
// todo: ideally this would come back with all the html it needs
el.innerHTML = newHtml + el.innerHTML;

// reset the inputs to blank
post.value = '';
cw.value = '';
} else {
console.error('HTTP PROXY CHANGE', Http);
// reset the inputs to blank
post.value = '';
cw.value = '';
} else {
console.error('HTTP PROXY CHANGE', Http);
}
}
}
});
return false;
},
replyTo: (activityId, mention) => {
Expand Down Expand Up @@ -265,4 +328,4 @@ const app = {
}
return false;
}
}
}
Loading