mirror of
https://github.com/overte-org/overte.git
synced 2025-04-29 22:22:37 +02:00
259 lines
9.6 KiB
QML
259 lines
9.6 KiB
QML
//
|
|
// Feed.qml
|
|
// qml/hifi
|
|
//
|
|
// Displays a particular type of feed
|
|
//
|
|
// Created by Howard Stearns on 4/18/2017
|
|
// Copyright 2016 High Fidelity, Inc.
|
|
//
|
|
// Distributed under the Apache License, Version 2.0.
|
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
|
//
|
|
|
|
import Hifi 1.0
|
|
import QtQuick 2.5
|
|
import QtGraphicalEffects 1.0
|
|
import "toolbars"
|
|
import "../styles-uit"
|
|
|
|
Column {
|
|
id: root;
|
|
visible: false;
|
|
|
|
property int cardWidth: 212;
|
|
property int cardHeight: 152;
|
|
property int textPadding: 10;
|
|
property int smallMargin: 4;
|
|
property int messageHeight: 40;
|
|
property int textSize: 24;
|
|
property int textSizeSmall: 18;
|
|
property int stackShadowNarrowing: 5;
|
|
property int stackedCardShadowHeight: 4;
|
|
property int labelSize: 20;
|
|
|
|
property string metaverseServerUrl: '';
|
|
property string protocol: '';
|
|
property string actions: 'snapshot';
|
|
// sendToScript doesn't get wired until after everything gets created. So we have to queue fillDestinations on nextTick.
|
|
property string labelText: actions;
|
|
property string filter: '';
|
|
onFilterChanged: filterChoicesByText();
|
|
property var goFunction: null;
|
|
property var rpc: null;
|
|
|
|
HifiConstants { id: hifi }
|
|
ListModel { id: suggestions; }
|
|
|
|
function resolveUrl(url) {
|
|
return (url.indexOf('/') === 0) ? (metaverseServerUrl + url) : url;
|
|
}
|
|
function makeModelData(data) { // create a new obj from data
|
|
// ListModel elements will only ever have those properties that are defined by the first obj that is added.
|
|
// So here we make sure that we have all the properties we need, regardless of whether it is a place data or user story.
|
|
var name = data.place_name,
|
|
tags = data.tags || [data.action, data.username],
|
|
description = data.description || "",
|
|
thumbnail_url = data.thumbnail_url || "";
|
|
if (actions === 'concurrency,snapshot') {
|
|
// A temporary hack for simulating announcements. We won't use this in production, but if requested, we'll use this data like announcements.
|
|
data.details.connections = 4;
|
|
data.action = 'announcement';
|
|
}
|
|
return {
|
|
place_name: name,
|
|
username: data.username || "",
|
|
path: data.path || "",
|
|
created_at: data.created_at || "",
|
|
action: data.action || "",
|
|
thumbnail_url: resolveUrl(thumbnail_url),
|
|
image_url: resolveUrl(data.details && data.details.image_url),
|
|
|
|
metaverseId: (data.id || "").toString(), // Some are strings from server while others are numbers. Model objects require uniformity.
|
|
|
|
tags: tags,
|
|
description: description,
|
|
online_users: data.details.connections || data.details.concurrency || 0,
|
|
drillDownToPlace: false,
|
|
|
|
searchText: [name].concat(tags, description || []).join(' ').toUpperCase()
|
|
}
|
|
}
|
|
property var allStories: [];
|
|
property var placeMap: ({}); // Used for making stacks.
|
|
property int requestId: 0;
|
|
function handleError(url, error, data, cb) { // cb(error) and answer truthy if needed, else falsey
|
|
if (!error && (data.status === 'success')) {
|
|
return;
|
|
}
|
|
if (!error) { // Create a message from the data
|
|
error = data.status + ': ' + data.error;
|
|
}
|
|
if (typeof(error) === 'string') { // Make a proper Error object
|
|
error = new Error(error);
|
|
}
|
|
error.message += ' in ' + url; // Include the url.
|
|
cb(error);
|
|
return true;
|
|
}
|
|
function getUserStoryPage(pageNumber, cb, cb1) { // cb(error) after all pages of domain data have been added to model
|
|
// If supplied, cb1 will be run after the first page IFF it is not the last, for responsiveness.
|
|
var options = [
|
|
'now=' + new Date().toISOString(),
|
|
'include_actions=' + actions,
|
|
'restriction=' + (Account.isLoggedIn() ? 'open,hifi' : 'open'),
|
|
'require_online=true',
|
|
'protocol=' + protocol,
|
|
'page=' + pageNumber
|
|
];
|
|
var url = metaverseBase + 'user_stories?' + options.join('&');
|
|
var thisRequestId = ++requestId;
|
|
rpc('request', url, function (error, data) {
|
|
if (thisRequestId !== requestId) {
|
|
error = 'stale';
|
|
}
|
|
if (handleError(url, error, data, cb)) {
|
|
return; // abandon stale requests
|
|
}
|
|
allStories = allStories.concat(data.user_stories.map(makeModelData));
|
|
if ((data.current_page < data.total_pages) && (data.current_page <= 10)) { // just 10 pages = 100 stories for now
|
|
if ((pageNumber === 1) && cb1) {
|
|
cb1();
|
|
}
|
|
return getUserStoryPage(pageNumber + 1, cb);
|
|
}
|
|
cb();
|
|
});
|
|
}
|
|
function fillDestinations() { // Public
|
|
console.debug('Feed::fillDestinations()')
|
|
|
|
function report(label, error) {
|
|
console.log(label, actions, error || 'ok', allStories.length, 'filtered to', suggestions.count);
|
|
}
|
|
var filter = makeFilteredStoryProcessor(), counter = 0;
|
|
allStories = [];
|
|
suggestions.clear();
|
|
placeMap = {};
|
|
getUserStoryPage(1, function (error) {
|
|
allStories.slice(counter).forEach(filter);
|
|
report('user stories update', error);
|
|
root.visible = !!suggestions.count;
|
|
}, function () { // If there's more than a page, put what we have in the model right away, keeping track of how many are processed.
|
|
allStories.forEach(function (story) {
|
|
counter++;
|
|
filter(story);
|
|
root.visible = !!suggestions.count;
|
|
});
|
|
report('user stories');
|
|
});
|
|
}
|
|
function identity(x) {
|
|
return x;
|
|
}
|
|
function makeFilteredStoryProcessor() { // answer a function(storyData) that adds it to suggestions if it matches
|
|
var words = filter.toUpperCase().split(/\s+/).filter(identity);
|
|
function suggestable(story) {
|
|
// We could filter out places we don't want to suggest, such as those where (story.place_name === AddressManager.placename) or (story.username === Account.username).
|
|
return true;
|
|
}
|
|
function matches(story) {
|
|
if (!words.length) {
|
|
return suggestable(story);
|
|
}
|
|
return words.every(function (word) {
|
|
return story.searchText.indexOf(word) >= 0;
|
|
});
|
|
}
|
|
function addToSuggestions(place) {
|
|
var collapse = ((actions === 'concurrency,snapshot') && (place.action !== 'concurrency')) || (place.action === 'announcement');
|
|
if (collapse) {
|
|
var existing = placeMap[place.place_name];
|
|
if (existing) {
|
|
existing.drillDownToPlace = true;
|
|
return;
|
|
}
|
|
}
|
|
suggestions.append(place);
|
|
if (collapse) {
|
|
placeMap[place.place_name] = suggestions.get(suggestions.count - 1);
|
|
} else if (place.action === 'concurrency') {
|
|
suggestions.get(suggestions.count - 1).drillDownToPlace = true; // Don't change raw place object (in allStories).
|
|
}
|
|
}
|
|
return function (story) {
|
|
if (matches(story)) {
|
|
addToSuggestions(story);
|
|
}
|
|
};
|
|
}
|
|
function filterChoicesByText() {
|
|
suggestions.clear();
|
|
placeMap = {};
|
|
allStories.forEach(makeFilteredStoryProcessor());
|
|
root.visible = !!suggestions.count;
|
|
}
|
|
|
|
RalewayBold {
|
|
id: label;
|
|
text: labelText;
|
|
color: hifi.colors.blueAccent;
|
|
size: labelSize;
|
|
}
|
|
ListView {
|
|
id: scroll;
|
|
model: suggestions;
|
|
orientation: ListView.Horizontal;
|
|
highlightFollowsCurrentItem: false
|
|
highlightMoveDuration: -1;
|
|
highlightMoveVelocity: -1;
|
|
currentIndex: -1;
|
|
|
|
spacing: 12;
|
|
width: parent.width;
|
|
height: cardHeight + stackedCardShadowHeight;
|
|
delegate: Card {
|
|
id: card;
|
|
width: cardWidth;
|
|
height: cardHeight;
|
|
goFunction: root.goFunction;
|
|
userName: model.username;
|
|
placeName: model.place_name;
|
|
hifiUrl: model.place_name + model.path;
|
|
thumbnail: model.thumbnail_url;
|
|
imageUrl: model.image_url;
|
|
action: model.action;
|
|
timestamp: model.created_at;
|
|
onlineUsers: model.online_users;
|
|
storyId: model.metaverseId;
|
|
drillDownToPlace: model.drillDownToPlace;
|
|
|
|
textPadding: root.textPadding;
|
|
smallMargin: root.smallMargin;
|
|
messageHeight: root.messageHeight;
|
|
textSize: root.textSize;
|
|
textSizeSmall: root.textSizeSmall;
|
|
stackShadowNarrowing: root.stackShadowNarrowing;
|
|
shadowHeight: root.stackedCardShadowHeight;
|
|
hoverThunk: function () { hovered = true }
|
|
unhoverThunk: function () { hovered = false }
|
|
}
|
|
}
|
|
NumberAnimation {
|
|
id: anim;
|
|
target: scroll;
|
|
property: "contentX";
|
|
duration: 250;
|
|
}
|
|
function scrollToIndex(index) {
|
|
anim.running = false;
|
|
var pos = scroll.contentX;
|
|
var destPos;
|
|
scroll.positionViewAtIndex(index, ListView.Contain);
|
|
destPos = scroll.contentX;
|
|
anim.from = pos;
|
|
anim.to = destPos;
|
|
scroll.currentIndex = index;
|
|
anim.running = true;
|
|
}
|
|
}
|