implement old/user/profile.js

This commit is contained in:
f0x 2022-09-12 19:34:04 +02:00
parent 10faf67b51
commit 43f2e988b9
11 changed files with 269 additions and 253 deletions

View file

@ -24,6 +24,7 @@
"factor-bundle": "^2.5.0",
"from2-string": "^1.1.0",
"icssify": "^2.0.0",
"is-plain-object": "^5.0.0",
"js-file-download": "^0.4.12",
"modern-normalize": "^1.1.0",
"photoswipe": "^5.3.0",

View file

@ -19,6 +19,7 @@
"use strict";
const Promise = require("bluebird");
const { isPlainObject } = require("is-plain-object");
const { APIError } = require("../errors");
const { setInstanceInfo } = require("../../redux/reducers/instances").actions;
@ -47,7 +48,13 @@ function apiCall(method, route, payload, type="json") {
} else if (type == "form") {
const formData = new FormData();
Object.entries(payload).forEach(([key, val]) => {
formData.set(key, val);
if (isPlainObject(val)) {
Object.entries(val).forEach(([key2, val2]) => {
formData.set(`${key}[${key2}]`, val2);
});
} else {
formData.set(key, val);
}
});
body = formData;
}

View file

@ -24,6 +24,38 @@ const d = require("dotty");
const user = require("../../redux/reducers/user").actions;
module.exports = function ({ apiCall }) {
function updateCredentials(selector, {formKeys=[], renamedKeys=[], fileKeys=[]}) {
return function (dispatch, getState) {
return Promise.try(() => {
const state = selector(getState());
const update = {};
formKeys.forEach((key) => {
d.put(update, key, d.get(state, key));
});
renamedKeys.forEach(([sendKey, intKey]) => {
d.put(update, sendKey, d.get(state, intKey));
});
fileKeys.forEach((key) => {
let file = d.get(state, `${key}File`);
if (file != undefined) {
d.put(update, key, file);
}
});
console.log(update);
return dispatch(apiCall("PATCH", "/api/v1/accounts/update_credentials", update, "form"));
}).then((account) => {
console.log(account);
return dispatch(user.setAccount(account));
});
};
}
return {
fetchAccount: function fetchAccount() {
return function (dispatch, _getState) {
@ -34,39 +66,17 @@ module.exports = function ({ apiCall }) {
});
};
},
updateAccount: function updateAccount() {
const formKeys = ["display_name", "locked"];
updateProfile: function updateProfile() {
const formKeys = ["display_name", "locked", "source"];
const renamedKeys = [["note", "source.note"]];
const fileKeys = ["header", "avatar"];
return function (dispatch, getState) {
return Promise.try(() => {
const { account } = getState().user;
return updateCredentials((state) => state.user.profile, {formKeys, renamedKeys, fileKeys});
},
updateSettings: function updateProfile() {
const formKeys = ["source"];
const update = {};
formKeys.forEach((key) => {
d.put(update, key, d.get(account, key));
update[key] = account[key];
});
renamedKeys.forEach(([sendKey, intKey]) => {
d.put(update, sendKey, d.get(account, intKey));
});
fileKeys.forEach((key) => {
let file = d.get(account, `${key}File`);
if (file != undefined) {
d.put(update, key, file);
}
});
return dispatch(apiCall("PATCH", "/api/v1/accounts/update_credentials", update, "form"));
}).then((account) => {
console.log(account);
return dispatch(user.setAccount(account));
});
};
return updateCredentials((state) => state.user.settings, {formKeys});
}
};
};

View file

@ -0,0 +1,50 @@
/*
GoToSocial
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
const d = require("dotty");
module.exports = function(dispatch, setter, obj) {
return {
onTextChange: function (key) {
return function (e) {
dispatch(setter([key, e.target.value]));
};
},
onCheckChange: function (key) {
return function (e) {
dispatch(setter([key, e.target.checked]));
};
},
onFileChange: function (key) {
return function (e) {
let old = d.get(obj, key);
if (old != undefined) {
URL.revokeObjectURL(old); // no error revoking a non-Object URL as provided by instance
}
let file = e.target.files[0];
let objectURL = URL.createObjectURL(file);
dispatch(setter([key, objectURL]));
dispatch(setter([`${key}File`, file]));
};
}
};
};

View file

@ -1,107 +0,0 @@
/*
GoToSocial
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
const React = require("react");
const Promise = require("bluebird");
const Languages = require("./languages");
const Submit = require("../../lib/submit");
module.exports = function Posts({oauth, account}) {
const [errorMsg, setError] = React.useState("");
const [statusMsg, setStatus] = React.useState("");
const [language, setLanguage] = React.useState("");
const [privacy, setPrivacy] = React.useState("");
const [format, setFormat] = React.useState("");
const [sensitive, setSensitive] = React.useState(false);
React.useEffect(() => {
if (account.source) {
setLanguage(account.source.language.toUpperCase());
setPrivacy(account.source.privacy);
setSensitive(account.source.sensitive ? account.source.sensitive : false);
setFormat(account.source.status_format ? account.source.status_format : "plain");
}
}, [account, setSensitive, setPrivacy]);
const submit = (e) => {
e.preventDefault();
setStatus("PATCHing");
setError("");
return Promise.try(() => {
let formDataInfo = new FormData();
formDataInfo.set("source[language]", language);
formDataInfo.set("source[privacy]", privacy);
formDataInfo.set("source[sensitive]", sensitive);
formDataInfo.set("source[status_format]", format);
return oauth.apiRequest("/api/v1/accounts/update_credentials", "PATCH", formDataInfo, "form");
}).then((json) => {
setStatus("Saved!");
setLanguage(json.source.language.toUpperCase());
setPrivacy(json.source.privacy);
setSensitive(json.source.sensitive ? json.source.sensitive : false);
setFormat(json.source.status_format ? json.source.status_format : "plain");
}).catch((e) => {
setError(e.message);
setStatus("");
});
};
return (
<section className="posts">
<h1>Post Settings</h1>
<form>
<div className="labelselect">
<label htmlFor="language">Default post language</label>
<select id="language" autoComplete="language" value={language} onChange={(e) => setLanguage(e.target.value)}>
<Languages />
</select>
</div>
<div className="labelselect">
<label htmlFor="privacy">Default post privacy</label>
<select id="privacy" value={privacy} onChange={(e) => setPrivacy(e.target.value)}>
<option value="private">Private / followers-only)</option>
<option value="unlisted">Unlisted</option>
<option value="public">Public</option>
</select>
<a href="https://docs.gotosocial.org/en/latest/user_guide/posts/#privacy-settings" target="_blank" className="moreinfolink" rel="noreferrer">Learn more about post privacy settings (opens in a new tab)</a>
</div>
<div className="labelselect">
<label htmlFor="format">Default post format</label>
<select id="format" value={format} onChange={(e) => setFormat(e.target.value)}>
<option value="plain">Plain (default)</option>
<option value="markdown">Markdown</option>
</select>
<a href="https://docs.gotosocial.org/en/latest/user_guide/posts/#input-types" target="_blank" className="moreinfolink" rel="noreferrer">Learn more about post format settings (opens in a new tab)</a>
</div>
<div className="labelcheckbox">
<label htmlFor="sensitive">Mark my posts as sensitive by default</label>
<input id="sensitive" type="checkbox" checked={sensitive} onChange={(e) => setSensitive(e.target.checked)}/>
</div>
<Submit onClick={submit} label="Save post settings" errorMsg={errorMsg} statusMsg={statusMsg}/>
</form>
</section>
);
};

View file

@ -24,13 +24,22 @@ const d = require("dotty");
module.exports = createSlice({
name: "user",
initialState: {
profile: {},
settings: {}
},
reducers: {
setAccount: (state, {payload}) => {
state.account = payload;
state.profile = payload;
// /user/settings only needs a copy of the 'source' obj
state.settings = {
source: payload.source
};
},
setAccountVal: (state, {payload: [key, val]}) => {
d.put(state.account, key, val);
setProfileVal: (state, {payload: [key, val]}) => {
d.put(state.profile, key, val);
},
setSettingsVal: (state, {payload: [key, val]}) => {
d.put(state.settings, key, val);
}
}
});

View file

@ -177,48 +177,11 @@ input, select, textarea {
) !important;
}
.user-profile {
section.with-sidebar > div {
display: flex;
flex-direction: column;
gap: 1rem;
.overview {
display: grid;
grid-template-columns: 1fr auto;
.basic {
margin-top: -4.5rem;
.avatar {
height: 5rem;
width: 5rem;
}
.displayname {
font-size: 1.3rem;
padding-top: 0;
padding-bottom: 0;
margin-top: 0.7rem;
}
}
.files {
padding: 1rem;
h3 {
margin-top: 0;
}
div:first-child {
margin-bottom: 1rem;
}
span {
font-style: italic;
}
}
}
input, textarea {
width: 100%;
line-height: 1.5rem;
@ -236,6 +199,7 @@ input, select, textarea {
input:invalid {
border-color: red;
}
textarea {
width: 100%;
height: 8rem;
@ -245,29 +209,6 @@ input, select, textarea {
margin-bottom: 0.5rem;
}
img {
display: flex;
justify-content: center;
align-items: center;
border: $boxshadow_border;
box-shadow: $box-shadow;
object-fit: cover;
border-radius: 0.2rem;
box-sizing: border-box;
margin-bottom: 0.5rem;
}
.avatarpreview {
height: 8.5rem;
width: 8.5rem;
}
.headerpreview {
width: 100%;
aspect-ratio: 3 / 1;
overflow: hidden;
}
.moreinfolink {
font-size: 0.9em;
}
@ -307,3 +248,60 @@ input, select, textarea {
gap: 0.4rem;
}
}
.user-profile {
.overview {
display: grid;
grid-template-columns: 70% 30%;
.basic {
margin-top: -4.5rem;
.avatar {
height: 5rem;
width: 5rem;
}
.displayname {
font-size: 1.3rem;
padding-top: 0;
padding-bottom: 0;
margin-top: 0.7rem;
}
}
.files {
margin: 1rem;
margin-right: 0;
display: flex;
flex-direction: column;
justify-content: center;
div.picker {
width: 100%;
display: flex;
span {
flex: 1 1 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 0.3rem 0;
}
}
h3 {
margin-top: 0;
margin-bottom: 0.5rem;
}
div:first-child {
margin-bottom: 1rem;
}
span {
font-style: italic;
}
}
}
}

View file

@ -21,103 +21,82 @@
const Promise = require("bluebird");
const React = require("react");
const Redux = require("react-redux");
const d = require("dotty");
const Submit = require("../components/submit");
const api = require("../lib/api");
const formFields = require("../lib/form-fields");
const user = require("../redux/reducers/user").actions;
module.exports = function UserProfile() {
const dispatch = Redux.useDispatch();
const account = Redux.useSelector(state => state.user.account);
const account = Redux.useSelector(state => state.user.profile);
const { onTextChange, onCheckChange, onFileChange } = formFields(dispatch, user.setProfileVal, account);
const [errorMsg, setError] = React.useState("");
const [statusMsg, setStatus] = React.useState("");
function onTextChange(key) {
return function (e) {
dispatch(user.setAccountVal([key, e.target.value]));
};
}
function onCheckChange(key) {
return function (e) {
dispatch(user.setAccountVal([key, e.target.checked]));
};
}
function onFileChange(key) {
return function (e) {
let old = d.get(account, key);
if (old != undefined) {
URL.revokeObjectURL(old); // no error revoking a non-Object URL as provided by instance
}
let file = e.target.files[0];
let objectURL = URL.createObjectURL(file);
dispatch(user.setAccountVal([key, objectURL]));
dispatch(user.setAccountVal([`${key}File`, file]));
};
}
const submit = (e) => {
e.preventDefault();
function submit() {
setStatus("PATCHing");
setError("");
return Promise.try(() => {
return dispatch(api.user.updateAccount());
return dispatch(api.user.updateProfile());
}).then(() => {
setStatus("Saved!");
}).catch((e) => {
setError(e.message);
setStatus("");
});
};
}
return (
<div className="user-profile">
<h1>Profile</h1>
<div className="overview">
<div className="profile">
<div className="headerimage">
<img className="headerpreview" src={account.header} alt={account.header ? `header image for ${account.username}` : "None set"}/>
</div>
<div className="basic">
<div id="profile-basic-filler2"></div>
<span className="avatar"><img className="avatarpreview" src={account.avatar} alt={account.avatar ? `avatar image for ${account.username}` : "None set"}/></span>
<div className="displayname">{account.display_name.trim().length > 0 ? account.display_name : account.username}</div>
<div className="username"><span>@{account.username}</span></div>
</div>
<div className="headerimage">
<img className="headerpreview" src={account.header} alt={account.header ? `header image for ${account.username}` : "None set"} />
</div>
<div className="basic">
<div id="profile-basic-filler2"></div>
<span className="avatar"><img className="avatarpreview" src={account.avatar} alt={account.avatar ? `avatar image for ${account.username}` : "None set"} /></span>
<div className="displayname">{account.display_name.trim().length > 0 ? account.display_name : account.username}</div>
<div className="username"><span>@{account.username}</span></div>
</div>
</div>
<div className="files">
<div>
<h3>Header</h3>
<label htmlFor="header" className="file-input button">Browse</label>
<span>{account.headerFile ? account.headerFile.name : "no file selected"}</span>
<input className="hidden" id="header" type="file" accept="image/*" onChange={onFileChange("header")}/>
<div className="picker">
<label htmlFor="header" className="file-input button">Browse</label>
<span>{account.headerFile ? account.headerFile.name : "no file selected"}</span>
</div>
<input className="hidden" id="header" type="file" accept="image/*" onChange={onFileChange("header")} />
</div>
<div>
<h3>Avatar</h3>
<label htmlFor="avatar" className="file-input button">Browse</label>
<span>{account.avatarFile ? account.avatarFile.name : "no file selected"}</span>
<input className="hidden" id="avatar" type="file" accept="image/*" onChange={onFileChange("avatar")}/>
<div className="picker">
<label htmlFor="avatar" className="file-input button">Browse</label>
<span>{account.avatarFile ? account.avatarFile.name : "no file selected"}</span>
</div>
<input className="hidden" id="avatar" type="file" accept="image/*" onChange={onFileChange("avatar")} />
</div>
</div>
</div>
<div className="labelinput">
<label htmlFor="displayname">Name</label>
<input id="displayname" type="text" value={account.display_name} onChange={onTextChange("display_name")} placeholder="A GoToSocial user"/>
<input id="displayname" type="text" value={account.display_name} onChange={onTextChange("display_name")} placeholder="A GoToSocial user" />
</div>
<div className="labelinput">
<label htmlFor="bio">Bio</label>
<textarea id="bio" value={account.source.note} onChange={onTextChange("source.note")} placeholder="Just trying out GoToSocial, my pronouns are they/them and I like sloths."/>
<textarea id="bio" value={account.source.note} onChange={onTextChange("source.note")} placeholder="Just trying out GoToSocial, my pronouns are they/them and I like sloths." />
</div>
<div className="labelcheckbox">
<label htmlFor="locked">Manually approve follow requests?</label>
<input id="locked" type="checkbox" checked={account.locked} onChange={onCheckChange("locked")}/>
<input id="locked" type="checkbox" checked={account.locked} onChange={onCheckChange("locked")} />
</div>
<Submit onClick={submit} label="Save profile info" errorMsg={errorMsg} statusMsg={statusMsg}/>
<Submit onClick={submit} label="Save profile info" errorMsg={errorMsg} statusMsg={statusMsg} />
</div>
);
};

View file

@ -18,6 +18,70 @@
"use strict";
const Promise = require("bluebird");
const React = require("react");
const Redux = require("react-redux");
const api = require("../lib/api");
const formFields = require("../lib/form-fields");
const user = require("../redux/reducers/user").actions;
const Languages = require("../components/languages");
const Submit = require("../components/submit");
module.exports = function UserSettings() {
return "user settings";
const dispatch = Redux.useDispatch();
const account = Redux.useSelector(state => state.user.settings);
const { onTextChange, onCheckChange } = formFields(dispatch, user.setSettingsVal, account);
const [errorMsg, setError] = React.useState("");
const [statusMsg, setStatus] = React.useState("");
function submit() {
setStatus("PATCHing");
setError("");
return Promise.try(() => {
return dispatch(api.user.updateSettings());
}).then(() => {
setStatus("Saved!");
}).catch((e) => {
setError(e.message);
setStatus("");
});
}
return (
<div className="user-settings">
<h1>Post settings</h1>
<div className="labelselect">
<label htmlFor="language">Default post language</label>
<select id="language" autoComplete="language" value={account.source.language.toUpperCase()} onChange={onTextChange("source.language")}>
<Languages />
</select>
</div>
<div className="labelselect">
<label htmlFor="privacy">Default post privacy</label>
<select id="privacy" value={account.source.privacy} onChange={onTextChange("source.privacy")}>
<option value="private">Private / followers-only)</option>
<option value="unlisted">Unlisted</option>
<option value="public">Public</option>
</select>
<a href="https://docs.gotosocial.org/en/latest/user_guide/posts/#privacy-settings" target="_blank" className="moreinfolink" rel="noreferrer">Learn more about post privacy settings (opens in a new tab)</a>
</div>
<div className="labelselect">
<label htmlFor="format">Default post format</label>
<select id="format" value={account.source.format} onChange={onTextChange("source.format")}>
<option value="plain">Plain (default)</option>
<option value="markdown">Markdown</option>
</select>
<a href="https://docs.gotosocial.org/en/latest/user_guide/posts/#input-types" target="_blank" className="moreinfolink" rel="noreferrer">Learn more about post format settings (opens in a new tab)</a>
</div>
<div className="labelcheckbox">
<label htmlFor="sensitive">Mark my posts as sensitive by default</label>
<input id="sensitive" type="checkbox" checked={account.source.sensitive} onChange={onCheckChange("source.sensitive")}/>
</div>
<Submit onClick={submit} label="Save post settings" errorMsg={errorMsg} statusMsg={statusMsg}/>
</div>
);
};

View file

@ -3670,6 +3670,11 @@ is-plain-obj@^2.0.0, is-plain-obj@^2.1.0:
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
is-plain-object@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
is-regex@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"