movil: usabilidad tras la prueba en el Pixel

Lo que salio de recorrer la app como alguien de la acampada:

- borrar (tribus y todo lo que usa renderContentActions) pide un segundo
  toque; la X va la ultima de la fila
- Karvan: Llamar empieza solo con voz (si se negaba la camara no arrancaba
  nada); botones con icono y nombre; textos traducidos; ayuda para invitar
- votaciones: tipo Si/No, semaforo o termometro (el servidor ya aceptaba
  options); resultados en barras; aviso a quien la crea de que no puede votar
- workflow activistas con tribus, parlamento, votaciones y eventos
- seccion apagada: /modules dice cual era y la activa de un toque
- Modulos en el movil: agrupados como el lanzador, interruptores y Guardar fijo
- tribus en filas compactas; secciones de una tribu en tira de fichas + Mas
- lanzador y barra con iconos SVG en vez de letras vai
- Ajustes: idioma y aspecto primero; idiomas de un toque
- aviso de bienvenida solo en la portada y en una linea; Ahora no al final
- pie de una linea con el estado de sincronizacion en claro
- mobile_ux.css para los dos skins: un margen de 14px (eran 74 por lado en
  las fichas), Anton solo en titulos y boton principal, buscador en una linea,
  la barra de abajo se esconde con el teclado
This commit is contained in:
SITO 2026-10-19 12:37:58 +02:00
parent 2e3ff1e79f
commit 4dd573662c
14 changed files with 1279 additions and 54 deletions

View file

@ -1,4 +1,4 @@
const { div, h2, p, section, button, form, a, textarea, br, input, table, tr, th, td, label, span } = require("../server/node_modules/hyperaxe");
const { div, h2, p, section, button, form, a, textarea, br, input, table, tr, th, td, label, span, progress } = require("../server/node_modules/hyperaxe");
const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view");
const { template, i18n, renderOpinionsVoting, renderEngagement, renderOpenClosedChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions, renderSpreadEditWarning, renderDocumentActions, renderModuleStatsBy, moduleIsEmpty, contentDeleteAction, paged, renderCreatedRows } = require("./main_views");
const moment = require("../server/node_modules/moment");
@ -7,6 +7,15 @@ const { renderTribeOriginChip } = require("./clearnet_view");
const userId = config.keys.id;
const VOTE_QUORUM = 2;
const isMobile = process.env.OASIS_MOBILE === "1";
// tipos de votacion en el movil: el servidor ya guarda las opciones que se le
// pasen (campo options), aqui solo se ofrecen las que usan las asambleas
const VOTE_KINDS = [
{ key: "classic", options: null },
{ key: "traffic", options: ["GREEN", "YELLOW", "RED"] },
{ key: "scale", options: ["1", "2", "3", "4", "5"] }
];
const safeArray = (v) => Array.isArray(v) ? v : [];
@ -21,7 +30,7 @@ const computeVoteOutcome = (baseCounts, voteOptions, totalVotesNum) => {
const topOpts = opts.filter((o) => (baseCounts[o] || 0) === maxCount);
if (maxCount === 0 || topOpts.length > 1) return noResult;
const winner = topOpts[0];
const variant = winner === "YES" ? "yes" : winner === "NO" ? "no" : "pending";
const variant = (winner === "YES" || winner === "GREEN") ? "yes" : (winner === "NO" || winner === "RED") ? "no" : "pending";
return { text: voteLabel(winner), variant };
};
@ -157,6 +166,22 @@ exports.renderVoteOptionsBlock = (v, voteOptions, action, returnTo, canVote) =>
];
};
// en el movil los resultados son barras, una por opcion: la tabla de 6 columnas
// no cabia y partia las palabras por silabas
const renderVoteBars = (voteOptions, baseCounts) => {
const total = voteOptions.reduce((s, o) => s + (Number(baseCounts[o]) || 0), 0);
return div({ class: "vote-bars" },
...voteOptions.map((opt) => {
const n = Number(baseCounts[opt]) || 0;
return div({ class: `vote-bar vote-bar-${String(opt).toLowerCase()}` },
span({ class: "vote-bar-label" }, voteLabel(opt)),
span({ class: "vote-bar-count" }, String(n)),
progress({ class: "vote-bar-meter", max: String(Math.max(total, 1)), value: String(n) })
);
})
);
};
const renderVoteDetail = (v, voteOptionsDefault, firstRow, secondRow, mode, activeFilter, params = {}) => {
const voteOptions = Array.isArray(v.options) && v.options.length ? v.options : voteOptionsDefault;
const baseCounts = voteOptions.reduce((acc, opt) => {
@ -212,7 +237,12 @@ const renderVoteDetail = (v, voteOptionsDefault, firstRow, secondRow, mode, acti
const voteButtonsNode = renderVoteButtons(v, voteOptions, firstRow, secondRow, returnTo);
const ownOpenNote = isMobile && !voteButtonsNode && isVoteAuthor(v) && normalizeStatus(v.status) === "OPEN"
? p({ class: "vote-own-note" }, i18n.voteOwnNote || "You created this vote, so you cannot vote in it.")
: null;
const voteMain = div({ class: "tribe-main" },
ownOpenNote,
voteButtonsNode
? div({ class: "job-section" },
h2({ class: "job-section-title" }, i18n.voteCastTitle || "Cast Vote"),
@ -224,7 +254,7 @@ const renderVoteDetail = (v, voteOptionsDefault, firstRow, secondRow, mode, acti
`${i18n.voteResults || "Results"}: `,
span({ class: `vote-outcome vote-outcome-${outcome.variant}` }, outcome.text)
),
div({ class: "vote-table" },
isMobile ? renderVoteBars(voteOptions, baseCounts) : div({ class: "vote-table" },
table(
tr(...voteOptions.map((opt) => th(voteLabel(opt)))),
tr(...voteOptions.map((opt) => td(baseCounts[opt])))
@ -247,6 +277,22 @@ const renderCommentsSection = (voteId, comments, activeFilter) => {
});
};
const renderVoteKindPicker = (current) => {
const cur = String(current || "");
return div({ class: "vote-kind" },
label({ class: "vote-kind-title" }, i18n.voteKindLabel || "Type of vote"),
...VOTE_KINDS.map((k, i) => {
const value = k.options ? k.options.join(",") : "";
const checked = cur ? cur === value : i === 0;
return label({ class: "vote-kind-option" },
input(Object.assign({ type: "radio", name: "options", value }, checked ? { checked: "checked" } : {})),
span({ class: "vote-kind-name" }, i18n["voteKind_" + k.key] || k.key),
span({ class: "vote-kind-hint" }, i18n["voteKindHint_" + k.key] || "")
);
})
);
};
exports.voteView = async (votes, mode, voteId, comments = [], activeFilterParam, params = {}) => {
const list = Array.isArray(votes) ? votes : [votes];
@ -351,6 +397,7 @@ exports.voteView = async (votes, mode, voteId, comments = [], activeFilterParam,
}), br(), br(),
label(i18n.voteTagsLabel), br(),
input({ type: "text", name: "tags", id: "tags", value: tagsValue }), br(), br(),
isMobile && mode === "create" ? renderVoteKindPicker(draft.options) : null,
button({ type: "submit" }, mode === "edit" ? i18n.voteUpdateButton : i18n.voteCreateButton)
)
)