upstream: integrar 0.9.5 en modelos y vistas medianas

chats_model: se adopta 0.9.5 y se reinyecta el replyTo de la rama (responder a
mensajes). El fix del salt en joinByInvite ya venia en upstream, y el limite de
mensajes pasa a ser configurable (60/h) en vez del parche que lo saltaba en movil.

config-manager: version de 0.9.5 con karvanMod, los seis modulos de public y toda
la normalizacion de bottomBarPins reinyectados; housingMod fuera. Entran blogsMod
y pollsMod del dev.

Se adopta 0.9.5 en votes_model (ya trae el margen de 2 minutos de la rama y anade
codigos de error), blobHandler (superconjunto compatible: mantiene handleBlobUpload
y anade subida multiple y deteccion de OGG), market_model (misma refactorizacion
mas opiniones y compradores), agenda_model y agenda_view (el codigo de 0.9.5 ya
degrada solo sin housingModel), opinions_view, cv_view y trending_view.

peers_view y parliament_view se quedan como estan: sus celdas llevan data-label,
que es lo que OasisMobile.css usa para convertir las tablas en tarjetas en movil.

Verificado con la app en marcha: 20 rutas OK, interfaz intacta (10 categorias, 47
modulos, 7 accesos rapidos, 0 etiquetas vacias) y Karvan completo de punta a punta
— crear sala, enviar y leer mensajes, mailbox de senalizacion WebRTC con miembros,
y la pagina de sala sirviendo karvan.js con el panel de videollamada.
This commit is contained in:
SITO 2026-08-18 22:38:09 +02:00
parent 82b6c3ae05
commit c662e37668
10 changed files with 310 additions and 40 deletions

View file

@ -25,6 +25,7 @@ function getViewDetailsAction(item) {
case 'job': return `/jobs/${encodeURIComponent(item.id)}`;
case 'project': return `/projects/${encodeURIComponent(item.id)}`;
case 'industry': return `/industry/build/${encodeURIComponent(item.id)}`;
case 'housing': return `/housing/${encodeURIComponent(item.id)}`;
case 'calendar': return `/calendars/${encodeURIComponent(item.id)}`;
default: return `/messages/${encodeURIComponent(item.id)}`;
}
@ -150,6 +151,21 @@ const renderAgendaItem = (item, userId, filter) => {
];
}
if (item.type === 'housing') {
const isOwner = String(item.author) === String(userId);
const requestCount = Number(item.requestCount) || 0;
details = [
renderCardField(i18n.housingType + ":", (i18n["housingType" + String(item.housing_type || '').toUpperCase()] || item.housing_type || '').toUpperCase()),
renderCardField(i18n.housingStatus + ":", String(item.status || '').toUpperCase() === 'CLOSED' ? i18n.housingStatusCLOSED : i18n.housingStatusOPEN),
item.place ? renderCardField(i18n.housingPlace + ":", item.place) : null,
renderCardField(i18n.housingPrice + ":", String(item.housing_type) === 'couchsurfing' ? (i18n.housingFree || 'FREE') : `${item.price} ECO`),
item.availableFrom ? renderCardField(i18n.housingAvailableFrom + ":", moment(item.availableFrom).format('YYYY/MM/DD')) : null,
isOwner
? renderCardField(i18n.housingRequests + ":", String(requestCount))
: renderCardField(i18n.housingRequests + ":", i18n.housingRequestedBadge || 'REQUESTED')
].filter(Boolean);
}
if (item.type === 'job') {
const subs = Array.isArray(item.subscribers)
? item.subscribers
@ -214,7 +230,7 @@ const renderAgendaItem = (item, userId, filter) => {
exports.agendaView = async (data, filter, q = '') => {
const { items = [], counts: _c = {} } = data || {};
const counts = { all: 0, open: 0, closed: 0, events: 0, tasks: 0, reports: 0, tribes: 0, jobs: 0, market: 0, projects: 0, transfers: 0, calendars: 0, discarded: 0, ..._c };
const counts = { all: 0, open: 0, closed: 0, events: 0, tasks: 0, reports: 0, tribes: 0, jobs: 0, market: 0, projects: 0, transfers: 0, calendars: 0, housing: 0, discarded: 0, ..._c };
return template(
i18n.agendaTitle,
section(
@ -252,6 +268,8 @@ exports.agendaView = async (data, filter, q = '') => {
`${i18n.agendaFilterProjects} (${counts.projects})`),
button({ type: 'submit', name: 'filter', value: 'industry', class: filter === 'industry' ? 'filter-btn active' : 'filter-btn' },
`${i18n.agendaFilterIndustry || 'INDUSTRY'} (${counts.industry || 0})`),
button({ type: 'submit', name: 'filter', value: 'housing', class: filter === 'housing' ? 'filter-btn active' : 'filter-btn' },
`${i18n.agendaFilterHousing || 'HOUSING'} (${counts.housing || 0})`),
button({ type: 'submit', name: 'filter', value: 'calendars', class: filter === 'calendars' ? 'filter-btn active' : 'filter-btn' },
`${i18n.agendaFilterCalendars || 'CALENDARS'} (${counts.calendars})`),
button({ type: 'submit', name: 'filter', value: 'transfers', class: filter === 'transfers' ? 'filter-btn active' : 'filter-btn' },

View file

@ -81,19 +81,19 @@ exports.createCVView = async (cv = {}, editMode = false) => {
input({ type: "text", name: "location", required: true, value: cv.location || "UNKNOWN" }), br(),
label(i18n.cvStatusLabel), br(),
select({ name: "status", required: true },
option({ value: "AVAILABLE", selected: cv.status === "AVAILABLE FOR COLLABORATION" }, "AVAILABLE FOR COLLABORATION"),
option({ value: "UNAVAILABLE", selected: cv.status === "NOT CURRENTLY AVAILABLE" }, "NOT CURRENTLY AVAILABLE"),
option({ value: "LOOKING FOR WORK", selected: !cv.status || cv.status === "LOOKING FOR WORK" }, "LOOKING FOR WORK")
option({ value: "AVAILABLE", ...(cv.status === "AVAILABLE FOR COLLABORATION" ? { selected: true } : {})}, "AVAILABLE FOR COLLABORATION"),
option({ value: "UNAVAILABLE", ...(cv.status === "NOT CURRENTLY AVAILABLE" ? { selected: true } : {})}, "NOT CURRENTLY AVAILABLE"),
option({ value: "LOOKING FOR WORK", ...(!cv.status || cv.status === "LOOKING FOR WORK" ? { selected: true } : {})}, "LOOKING FOR WORK")
), br(), br(),
label(i18n.cvPreferencesLabel), br(),
select({ name: "preferences", required: true },
option({ value: "IN PERSON", selected: cv.preferences === "IN-PERSON ONLY" }, "IN-PERSON ONLY"),
option({ value: "REMOTE WORKING", selected: !cv.preferences || cv.preferences === "REMOTE WORKING" }, "REMOTE-WORKING")
option({ value: "IN PERSON", ...(cv.preferences === "IN-PERSON ONLY" ? { selected: true } : {})}, "IN-PERSON ONLY"),
option({ value: "REMOTE WORKING", ...(!cv.preferences || cv.preferences === "REMOTE WORKING" ? { selected: true } : {})}, "REMOTE-WORKING")
), br(), br(),
label(i18n.visibilityLabel || "Visibility"), br(),
select({ name: "visibility" },
option({ value: "PUBLIC", selected: (cv.visibility || "PUBLIC") === "PUBLIC" }, i18n.visibilityPublic || "Public"),
option({ value: "HIDDEN", selected: cv.visibility === "HIDDEN" }, i18n.visibilityHidden || "Hidden")
option({ value: "PUBLIC", ...((cv.visibility || "PUBLIC") === "PUBLIC" ? { selected: true } : {})}, i18n.visibilityPublic || "Public"),
option({ value: "HIDDEN", ...(cv.visibility === "HIDDEN" ? { selected: true } : {})}, i18n.visibilityHidden || "Hidden")
), br()
], "availability"),

View file

@ -25,6 +25,8 @@ const detailHref = (type, key) => {
case 'task': return `/tasks/${encodeURIComponent(key)}`;
case 'event': return `/events/${encodeURIComponent(key)}`;
case 'shopProduct': return `/shops/product/${encodeURIComponent(key)}`;
case 'housing': return `/housing/${encodeURIComponent(key)}`;
case 'market': return `/market/${encodeURIComponent(key)}`;
default: return null;
}
};
@ -71,6 +73,76 @@ const renderContentHtml = (content, key) => {
content.description ? p(...renderUrl(content.description)) : null
)
);
case 'housing': {
const free = String(content.housing_type || '').toLowerCase() === 'couchsurfing';
return div({ class: 'opinion-housing' },
div({ class: 'card-section housing' },
div({ class: 'card-field' },
span({ class: 'card-label' }, i18n.title + ':'),
span({ class: 'card-value' }, content.title || '')
),
content.housing_type ? div({ class: 'card-field' },
span({ class: 'card-label' }, i18n.housingType + ':'),
span({ class: 'card-value' }, String(i18n['housingType' + String(content.housing_type).toUpperCase()] || content.housing_type).toUpperCase())
) : null,
content.property_type ? div({ class: 'card-field' },
span({ class: 'card-label' }, i18n.housingProperty + ':'),
span({ class: 'card-value' }, String(i18n['housingProperty' + String(content.property_type).toUpperCase()] || content.property_type).toUpperCase())
) : null,
div({ class: 'card-field' },
span({ class: 'card-label' }, i18n.housingPrice + ':'),
span({ class: 'card-value' }, free ? (i18n.housingFree || 'FREE') : `${Number(content.price || 0).toFixed(2)} ECO`)
),
content.place ? div({ class: 'card-field' },
span({ class: 'card-label' }, i18n.housingPlace + ':'),
span({ class: 'card-value' }, content.place)
) : null,
Number(content.rooms) > 0 ? div({ class: 'card-field' },
span({ class: 'card-label' }, i18n.housingRooms + ':'),
span({ class: 'card-value' }, String(content.rooms))
) : null,
Number(content.size) > 0 ? div({ class: 'card-field' },
span({ class: 'card-label' }, i18n.housingSize + ':'),
span({ class: 'card-value' }, `${content.size} m²`)
) : null,
Number(content.capacity) > 0 ? div({ class: 'card-field' },
span({ class: 'card-label' }, i18n.housingCapacity + ':'),
span({ class: 'card-value' }, String(content.capacity))
) : null,
content.description ? p(...renderUrl(content.description)) : null,
Array.isArray(content.tags) && content.tags.length
? div({ class: 'card-tags' }, content.tags.map(tag =>
a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)))
: null
)
);
}
case 'market':
return div({ class: 'opinion-market' },
div({ class: 'card-section market' },
div({ class: 'card-field' },
span({ class: 'card-label' }, i18n.title + ':'),
span({ class: 'card-value' }, content.title || '')
),
content.item_type ? div({ class: 'card-field' },
span({ class: 'card-label' }, i18n.marketItemType + ':'),
span({ class: 'card-value' }, String(content.item_type).toUpperCase())
) : null,
content.item_status ? div({ class: 'card-field' },
span({ class: 'card-label' }, (i18n.marketItemCondition || i18n.status) + ':'),
span({ class: 'card-value' }, String(content.item_status).toUpperCase())
) : null,
div({ class: 'card-field' },
span({ class: 'card-label' }, (i18n.marketItemPrice || i18n.price) + ':'),
span({ class: 'card-value' }, `${content.price} ECO`)
),
content.description ? p(...renderUrl(content.description)) : null,
Array.isArray(content.tags) && content.tags.length
? div({ class: 'card-tags' }, content.tags.map(tag =>
a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)))
: null
)
);
case 'bookmark':
return div({ class: 'opinion-bookmark' },
div({ class: 'card-section bookmark' },

View file

@ -118,6 +118,36 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new
c.description ? p(...renderUrl(c.description)) : null
)
);
} else if (c.type === 'housing') {
const free = String(c.housing_type || '').toLowerCase() === 'couchsurfing';
contentHtml = div({ class: 'trending-housing' },
div({ class: 'card-section housing' },
div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, c.title || '')),
c.housing_type ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.housingType + ':'), span({ class: 'card-value' }, String(i18n['housingType' + String(c.housing_type).toUpperCase()] || c.housing_type).toUpperCase())) : "",
c.property_type ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.housingProperty + ':'), span({ class: 'card-value' }, String(i18n['housingProperty' + String(c.property_type).toUpperCase()] || c.property_type).toUpperCase())) : "",
div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.housingPrice + ':'), span({ class: 'card-value' }, free ? (i18n.housingFree || 'FREE') : `${Number(c.price || 0).toFixed(2)} ECO`)),
c.place ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.housingPlace + ':'), span({ class: 'card-value' }, c.place)) : "",
Number(c.rooms) > 0 ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.housingRooms + ':'), span({ class: 'card-value' }, String(c.rooms))) : "",
Number(c.size) > 0 ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.housingSize + ':'), span({ class: 'card-value' }, `${c.size} m²`)) : "",
c.description ? p(...renderUrl(c.description)) : null,
Array.isArray(c.tags) && c.tags.length
? div({ class: 'card-tags' }, c.tags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)))
: null
)
);
} else if (c.type === 'market') {
contentHtml = div({ class: 'trending-market' },
div({ class: 'card-section market' },
div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, c.title || '')),
c.item_type ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemType + ':'), span({ class: 'card-value' }, String(c.item_type).toUpperCase())) : "",
c.item_status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.marketItemCondition || i18n.status) + ':'), span({ class: 'card-value' }, String(c.item_status).toUpperCase())) : "",
div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.marketItemPrice || i18n.price) + ':'), span({ class: 'card-value' }, `${c.price} ECO`)),
c.description ? p(...renderUrl(c.description)) : null,
Array.isArray(c.tags) && c.tags.length
? div({ class: 'card-tags' }, c.tags.map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' }, `#${tag}`)))
: null
)
);
} else if (c.type === 'feed') {
const { text, refeeds } = c;
contentHtml = div({ class: 'trending-feed' },
@ -183,7 +213,9 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new
report: 'reports',
task: 'tasks',
event: 'events',
shopProduct: 'shops/product'
shopProduct: 'shops/product',
housing: 'housing',
market: 'market'
};
const detailHref = detailPaths[c.type]
? `/${detailPaths[c.type]}/${encodeURIComponent(item.key)}`