diff --git a/web/source/package.json b/web/source/package.json index 4467265f2..412c92329 100644 --- a/web/source/package.json +++ b/web/source/package.json @@ -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", diff --git a/web/source/settings-panel/lib/languages.js b/web/source/settings-panel/components/languages.jsx similarity index 100% rename from web/source/settings-panel/lib/languages.js rename to web/source/settings-panel/components/languages.jsx diff --git a/web/source/settings-panel/lib/api/index.js b/web/source/settings-panel/lib/api/index.js index 23aa07a96..6240f9455 100644 --- a/web/source/settings-panel/lib/api/index.js +++ b/web/source/settings-panel/lib/api/index.js @@ -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; } diff --git a/web/source/settings-panel/lib/api/user.js b/web/source/settings-panel/lib/api/user.js index 2af800d41..32309a2f8 100644 --- a/web/source/settings-panel/lib/api/user.js +++ b/web/source/settings-panel/lib/api/user.js @@ -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}); } }; }; \ No newline at end of file diff --git a/web/source/settings-panel/lib/form-fields.js b/web/source/settings-panel/lib/form-fields.js new file mode 100644 index 000000000..dfdd93086 --- /dev/null +++ b/web/source/settings-panel/lib/form-fields.js @@ -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 . +*/ + +"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])); + }; + } + }; +}; diff --git a/web/source/settings-panel/old/user/posts.js b/web/source/settings-panel/old/user/posts.js deleted file mode 100644 index 333a8ae24..000000000 --- a/web/source/settings-panel/old/user/posts.js +++ /dev/null @@ -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 . -*/ - -"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 ( -
-

Post Settings

-
-
- - -
-
- - - Learn more about post privacy settings (opens in a new tab) -
-
- - - Learn more about post format settings (opens in a new tab) -
-
- - setSensitive(e.target.checked)}/> -
- - -
- ); -}; diff --git a/web/source/settings-panel/redux/reducers/user.js b/web/source/settings-panel/redux/reducers/user.js index 1cc4e894e..673ed33d9 100644 --- a/web/source/settings-panel/redux/reducers/user.js +++ b/web/source/settings-panel/redux/reducers/user.js @@ -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); } } }); \ No newline at end of file diff --git a/web/source/settings-panel/style.css b/web/source/settings-panel/style.css index e2d6f20c6..68350239c 100644 --- a/web/source/settings-panel/style.css +++ b/web/source/settings-panel/style.css @@ -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; + } + } + } +} diff --git a/web/source/settings-panel/user/profile.js b/web/source/settings-panel/user/profile.js index b3ac44dc3..a74623be7 100644 --- a/web/source/settings-panel/user/profile.js +++ b/web/source/settings-panel/user/profile.js @@ -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 (

Profile

-
- {account.header -
-
-
- {account.avatar -
{account.display_name.trim().length > 0 ? account.display_name : account.username}
-
@{account.username}
-
+
+ {account.header +
+
+
+ {account.avatar +
{account.display_name.trim().length > 0 ? account.display_name : account.username}
+
@{account.username}
+

Header

- - {account.headerFile ? account.headerFile.name : "no file selected"} - +
+ + {account.headerFile ? account.headerFile.name : "no file selected"} +
+

Avatar

- - {account.avatarFile ? account.avatarFile.name : "no file selected"} - +
+ + {account.avatarFile ? account.avatarFile.name : "no file selected"} +
+
- +
-