This app provides monitoring and information features for the common freifunk user and the technical stuff of a freifunk community.
Code base is taken from a TUM Practical Course project and added here to see if Freifunk Altdorf can use it.
https://www.freifunk-altdorf.de
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
527 lines
16 KiB
527 lines
16 KiB
/** |
|
* Copyright (c) 2015-present, Facebook, Inc. |
|
* |
|
* This source code is licensed under the MIT license found in the |
|
* LICENSE file in the root directory of this source tree. |
|
* |
|
* @providesModule VirtualizedSectionList |
|
* @flow |
|
* @format |
|
*/ |
|
'use strict'; |
|
|
|
const React = require('React'); |
|
const View = require('View'); |
|
const VirtualizedList = require('VirtualizedList'); |
|
|
|
const invariant = require('fbjs/lib/invariant'); |
|
|
|
import type {ViewToken} from 'ViewabilityHelper'; |
|
import type {Props as VirtualizedListProps} from 'VirtualizedList'; |
|
|
|
type Item = any; |
|
type SectionItem = any; |
|
|
|
type SectionBase = { |
|
// Must be provided directly on each section. |
|
data: $ReadOnlyArray<SectionItem>, |
|
key?: string, |
|
|
|
// Optional props will override list-wide props just for this section. |
|
renderItem?: ?({ |
|
item: SectionItem, |
|
index: number, |
|
section: SectionBase, |
|
separators: { |
|
highlight: () => void, |
|
unhighlight: () => void, |
|
updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, |
|
}, |
|
}) => ?React.Element<any>, |
|
ItemSeparatorComponent?: ?React.ComponentType<*>, |
|
keyExtractor?: (item: SectionItem, index: ?number) => string, |
|
|
|
// TODO: support more optional/override props |
|
// FooterComponent?: ?ReactClass<*>, |
|
// HeaderComponent?: ?ReactClass<*>, |
|
// onViewableItemsChanged?: ({viewableItems: Array<ViewToken>, changed: Array<ViewToken>}) => void, |
|
}; |
|
|
|
type RequiredProps<SectionT: SectionBase> = { |
|
sections: $ReadOnlyArray<SectionT>, |
|
}; |
|
|
|
type OptionalProps<SectionT: SectionBase> = { |
|
/** |
|
* Rendered after the last item in the last section. |
|
*/ |
|
ListFooterComponent?: ?(React.ComponentType<*> | React.Element<any>), |
|
/** |
|
* Rendered at the very beginning of the list. |
|
*/ |
|
ListHeaderComponent?: ?(React.ComponentType<*> | React.Element<any>), |
|
/** |
|
* Default renderer for every item in every section. |
|
*/ |
|
renderItem?: (info: { |
|
item: Item, |
|
index: number, |
|
section: SectionT, |
|
separators: { |
|
highlight: () => void, |
|
unhighlight: () => void, |
|
updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, |
|
}, |
|
}) => ?React.Element<any>, |
|
/** |
|
* Rendered at the top of each section. |
|
*/ |
|
renderSectionHeader?: ?({section: SectionT}) => ?React.Element<any>, |
|
/** |
|
* Rendered at the bottom of each section. |
|
*/ |
|
renderSectionFooter?: ?({section: SectionT}) => ?React.Element<any>, |
|
/** |
|
* Rendered at the bottom of every Section, except the very last one, in place of the normal |
|
* ItemSeparatorComponent. |
|
*/ |
|
SectionSeparatorComponent?: ?React.ComponentType<*>, |
|
/** |
|
* Rendered at the bottom of every Item except the very last one in the last section. |
|
*/ |
|
ItemSeparatorComponent?: ?React.ComponentType<*>, |
|
/** |
|
* Warning: Virtualization can drastically improve memory consumption for long lists, but trashes |
|
* the state of items when they scroll out of the render window, so make sure all relavent data is |
|
* stored outside of the recursive `renderItem` instance tree. |
|
*/ |
|
enableVirtualization?: ?boolean, |
|
keyExtractor: (item: Item, index: number) => string, |
|
onEndReached?: ?({distanceFromEnd: number}) => void, |
|
/** |
|
* If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make |
|
* sure to also set the `refreshing` prop correctly. |
|
*/ |
|
onRefresh?: ?Function, |
|
/** |
|
* Called when the viewability of rows changes, as defined by the |
|
* `viewabilityConfig` prop. |
|
*/ |
|
onViewableItemsChanged?: ?({ |
|
viewableItems: Array<ViewToken>, |
|
changed: Array<ViewToken>, |
|
}) => void, |
|
/** |
|
* Set this true while waiting for new data from a refresh. |
|
*/ |
|
refreshing?: ?boolean, |
|
}; |
|
|
|
export type Props<SectionT> = RequiredProps<SectionT> & |
|
OptionalProps<SectionT> & |
|
VirtualizedListProps; |
|
|
|
type DefaultProps = typeof VirtualizedList.defaultProps & { |
|
data: $ReadOnlyArray<Item>, |
|
}; |
|
type State = {childProps: VirtualizedListProps}; |
|
|
|
/** |
|
* Right now this just flattens everything into one list and uses VirtualizedList under the |
|
* hood. The only operation that might not scale well is concatting the data arrays of all the |
|
* sections when new props are received, which should be plenty fast for up to ~10,000 items. |
|
*/ |
|
class VirtualizedSectionList<SectionT: SectionBase> extends React.PureComponent< |
|
Props<SectionT>, |
|
State, |
|
> { |
|
props: Props<SectionT>; |
|
|
|
state: State; |
|
|
|
static defaultProps: DefaultProps = { |
|
...VirtualizedList.defaultProps, |
|
data: [], |
|
}; |
|
|
|
scrollToLocation(params: { |
|
animated?: ?boolean, |
|
itemIndex: number, |
|
sectionIndex: number, |
|
viewPosition?: number, |
|
}) { |
|
let index = params.itemIndex + 1; |
|
for (let ii = 0; ii < params.sectionIndex; ii++) { |
|
index += this.props.sections[ii].data.length + 2; |
|
} |
|
const toIndexParams = { |
|
...params, |
|
index, |
|
}; |
|
this._listRef.scrollToIndex(toIndexParams); |
|
} |
|
|
|
getListRef(): VirtualizedList { |
|
return this._listRef; |
|
} |
|
|
|
_keyExtractor = (item: Item, index: number) => { |
|
const info = this._subExtractor(index); |
|
return (info && info.key) || String(index); |
|
}; |
|
|
|
_subExtractor( |
|
index: number, |
|
): ?{ |
|
section: SectionT, |
|
key: string, // Key of the section or combined key for section + item |
|
index: ?number, // Relative index within the section |
|
header?: ?boolean, // True if this is the section header |
|
leadingItem?: ?Item, |
|
leadingSection?: ?SectionT, |
|
trailingItem?: ?Item, |
|
trailingSection?: ?SectionT, |
|
} { |
|
let itemIndex = index; |
|
const defaultKeyExtractor = this.props.keyExtractor; |
|
for (let ii = 0; ii < this.props.sections.length; ii++) { |
|
const section = this.props.sections[ii]; |
|
const key = section.key || String(ii); |
|
itemIndex -= 1; // The section adds an item for the header |
|
if (itemIndex >= section.data.length + 1) { |
|
itemIndex -= section.data.length + 1; // The section adds an item for the footer. |
|
} else if (itemIndex === -1) { |
|
return { |
|
section, |
|
key: key + ':header', |
|
index: null, |
|
header: true, |
|
trailingSection: this.props.sections[ii + 1], |
|
}; |
|
} else if (itemIndex === section.data.length) { |
|
return { |
|
section, |
|
key: key + ':footer', |
|
index: null, |
|
header: false, |
|
trailingSection: this.props.sections[ii + 1], |
|
}; |
|
} else { |
|
const keyExtractor = section.keyExtractor || defaultKeyExtractor; |
|
return { |
|
section, |
|
key: key + ':' + keyExtractor(section.data[itemIndex], itemIndex), |
|
index: itemIndex, |
|
leadingItem: section.data[itemIndex - 1], |
|
leadingSection: this.props.sections[ii - 1], |
|
trailingItem: section.data[itemIndex + 1], |
|
trailingSection: this.props.sections[ii + 1], |
|
}; |
|
} |
|
} |
|
} |
|
|
|
_convertViewable = (viewable: ViewToken): ?ViewToken => { |
|
invariant(viewable.index != null, 'Received a broken ViewToken'); |
|
const info = this._subExtractor(viewable.index); |
|
if (!info) { |
|
return null; |
|
} |
|
const keyExtractor = info.section.keyExtractor || this.props.keyExtractor; |
|
return { |
|
...viewable, |
|
index: info.index, |
|
/* $FlowFixMe(>=0.63.0 site=react_native_fb) This comment suppresses an |
|
* error found when Flow v0.63 was deployed. To see the error delete this |
|
* comment and run Flow. */ |
|
key: keyExtractor(viewable.item, info.index), |
|
section: info.section, |
|
}; |
|
}; |
|
|
|
_onViewableItemsChanged = ({ |
|
viewableItems, |
|
changed, |
|
}: { |
|
viewableItems: Array<ViewToken>, |
|
changed: Array<ViewToken>, |
|
}) => { |
|
if (this.props.onViewableItemsChanged) { |
|
this.props.onViewableItemsChanged({ |
|
viewableItems: viewableItems |
|
.map(this._convertViewable, this) |
|
.filter(Boolean), |
|
changed: changed.map(this._convertViewable, this).filter(Boolean), |
|
}); |
|
} |
|
}; |
|
|
|
_renderItem = ({item, index}: {item: Item, index: number}) => { |
|
const info = this._subExtractor(index); |
|
if (!info) { |
|
return null; |
|
} |
|
const infoIndex = info.index; |
|
if (infoIndex == null) { |
|
const {section} = info; |
|
if (info.header === true) { |
|
const {renderSectionHeader} = this.props; |
|
return renderSectionHeader ? renderSectionHeader({section}) : null; |
|
} else { |
|
const {renderSectionFooter} = this.props; |
|
return renderSectionFooter ? renderSectionFooter({section}) : null; |
|
} |
|
} else { |
|
const renderItem = info.section.renderItem || this.props.renderItem; |
|
const SeparatorComponent = this._getSeparatorComponent(index, info); |
|
invariant(renderItem, 'no renderItem!'); |
|
return ( |
|
<ItemWithSeparator |
|
SeparatorComponent={SeparatorComponent} |
|
LeadingSeparatorComponent={ |
|
infoIndex === 0 ? this.props.SectionSeparatorComponent : undefined |
|
} |
|
cellKey={info.key} |
|
index={infoIndex} |
|
item={item} |
|
leadingItem={info.leadingItem} |
|
leadingSection={info.leadingSection} |
|
onUpdateSeparator={this._onUpdateSeparator} |
|
prevCellKey={(this._subExtractor(index - 1) || {}).key} |
|
ref={ref => { |
|
this._cellRefs[info.key] = ref; |
|
}} |
|
renderItem={renderItem} |
|
section={info.section} |
|
trailingItem={info.trailingItem} |
|
trailingSection={info.trailingSection} |
|
/> |
|
); |
|
} |
|
}; |
|
|
|
_onUpdateSeparator = (key: string, newProps: Object) => { |
|
const ref = this._cellRefs[key]; |
|
ref && ref.updateSeparatorProps(newProps); |
|
}; |
|
|
|
_getSeparatorComponent( |
|
index: number, |
|
info?: ?Object, |
|
): ?React.ComponentType<*> { |
|
info = info || this._subExtractor(index); |
|
if (!info) { |
|
return null; |
|
} |
|
const ItemSeparatorComponent = |
|
info.section.ItemSeparatorComponent || this.props.ItemSeparatorComponent; |
|
const {SectionSeparatorComponent} = this.props; |
|
const isLastItemInList = index === this.state.childProps.getItemCount() - 1; |
|
const isLastItemInSection = info.index === info.section.data.length - 1; |
|
if (SectionSeparatorComponent && isLastItemInSection) { |
|
return SectionSeparatorComponent; |
|
} |
|
if (ItemSeparatorComponent && !isLastItemInSection && !isLastItemInList) { |
|
return ItemSeparatorComponent; |
|
} |
|
return null; |
|
} |
|
|
|
_computeState(props: Props<SectionT>): State { |
|
const offset = props.ListHeaderComponent ? 1 : 0; |
|
const stickyHeaderIndices = []; |
|
const itemCount = props.sections.reduce((v, section) => { |
|
stickyHeaderIndices.push(v + offset); |
|
return v + section.data.length + 2; // Add two for the section header and footer. |
|
}, 0); |
|
|
|
return { |
|
childProps: { |
|
...props, |
|
renderItem: this._renderItem, |
|
ItemSeparatorComponent: undefined, // Rendered with renderItem |
|
data: props.sections, |
|
getItemCount: () => itemCount, |
|
getItem, |
|
keyExtractor: this._keyExtractor, |
|
onViewableItemsChanged: props.onViewableItemsChanged |
|
? this._onViewableItemsChanged |
|
: undefined, |
|
stickyHeaderIndices: props.stickySectionHeadersEnabled |
|
? stickyHeaderIndices |
|
: undefined, |
|
}, |
|
}; |
|
} |
|
|
|
constructor(props: Props<SectionT>, context: Object) { |
|
super(props, context); |
|
this.state = this._computeState(props); |
|
} |
|
|
|
UNSAFE_componentWillReceiveProps(nextProps: Props<SectionT>) { |
|
this.setState(this._computeState(nextProps)); |
|
} |
|
|
|
render() { |
|
return ( |
|
<VirtualizedList {...this.state.childProps} ref={this._captureRef} /> |
|
); |
|
} |
|
|
|
_cellRefs = {}; |
|
_listRef: VirtualizedList; |
|
_captureRef = ref => { |
|
/* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This comment |
|
* suppresses an error when upgrading Flow's support for React. To see the |
|
* error delete this comment and run Flow. */ |
|
this._listRef = ref; |
|
}; |
|
} |
|
|
|
type ItemWithSeparatorProps = { |
|
LeadingSeparatorComponent: ?React.ComponentType<*>, |
|
SeparatorComponent: ?React.ComponentType<*>, |
|
cellKey: string, |
|
index: number, |
|
item: Item, |
|
onUpdateSeparator: (cellKey: string, newProps: Object) => void, |
|
prevCellKey?: ?string, |
|
renderItem: Function, |
|
section: Object, |
|
leadingItem: ?Item, |
|
leadingSection: ?Object, |
|
trailingItem: ?Item, |
|
trailingSection: ?Object, |
|
}; |
|
|
|
class ItemWithSeparator extends React.Component< |
|
ItemWithSeparatorProps, |
|
$FlowFixMeState, |
|
> { |
|
state = { |
|
separatorProps: { |
|
highlighted: false, |
|
leadingItem: this.props.item, |
|
leadingSection: this.props.leadingSection, |
|
section: this.props.section, |
|
trailingItem: this.props.trailingItem, |
|
trailingSection: this.props.trailingSection, |
|
}, |
|
leadingSeparatorProps: { |
|
highlighted: false, |
|
leadingItem: this.props.leadingItem, |
|
leadingSection: this.props.leadingSection, |
|
section: this.props.section, |
|
trailingItem: this.props.item, |
|
trailingSection: this.props.trailingSection, |
|
}, |
|
}; |
|
|
|
_separators = { |
|
highlight: () => { |
|
['leading', 'trailing'].forEach(s => |
|
this._separators.updateProps(s, {highlighted: true}), |
|
); |
|
}, |
|
unhighlight: () => { |
|
['leading', 'trailing'].forEach(s => |
|
this._separators.updateProps(s, {highlighted: false}), |
|
); |
|
}, |
|
updateProps: (select: 'leading' | 'trailing', newProps: Object) => { |
|
const {LeadingSeparatorComponent, cellKey, prevCellKey} = this.props; |
|
if (select === 'leading' && LeadingSeparatorComponent) { |
|
this.setState(state => ({ |
|
leadingSeparatorProps: {...state.leadingSeparatorProps, ...newProps}, |
|
})); |
|
} else { |
|
this.props.onUpdateSeparator( |
|
(select === 'leading' && prevCellKey) || cellKey, |
|
newProps, |
|
); |
|
} |
|
}, |
|
}; |
|
|
|
UNSAFE_componentWillReceiveProps(props: ItemWithSeparatorProps) { |
|
this.setState(state => ({ |
|
separatorProps: { |
|
...this.state.separatorProps, |
|
leadingItem: props.item, |
|
leadingSection: props.leadingSection, |
|
section: props.section, |
|
trailingItem: props.trailingItem, |
|
trailingSection: props.trailingSection, |
|
}, |
|
leadingSeparatorProps: { |
|
...this.state.leadingSeparatorProps, |
|
leadingItem: props.leadingItem, |
|
leadingSection: props.leadingSection, |
|
section: props.section, |
|
trailingItem: props.item, |
|
trailingSection: props.trailingSection, |
|
}, |
|
})); |
|
} |
|
|
|
updateSeparatorProps(newProps: Object) { |
|
this.setState(state => ({ |
|
separatorProps: {...state.separatorProps, ...newProps}, |
|
})); |
|
} |
|
|
|
render() { |
|
const { |
|
LeadingSeparatorComponent, |
|
SeparatorComponent, |
|
item, |
|
index, |
|
section, |
|
} = this.props; |
|
const element = this.props.renderItem({ |
|
item, |
|
index, |
|
section, |
|
separators: this._separators, |
|
}); |
|
const leadingSeparator = LeadingSeparatorComponent && ( |
|
<LeadingSeparatorComponent {...this.state.leadingSeparatorProps} /> |
|
); |
|
const separator = SeparatorComponent && ( |
|
<SeparatorComponent {...this.state.separatorProps} /> |
|
); |
|
return leadingSeparator || separator ? ( |
|
<View> |
|
{leadingSeparator} |
|
{element} |
|
{separator} |
|
</View> |
|
) : ( |
|
element |
|
); |
|
} |
|
} |
|
|
|
function getItem(sections: ?$ReadOnlyArray<Item>, index: number): ?Item { |
|
if (!sections) { |
|
return null; |
|
} |
|
let itemIdx = index - 1; |
|
for (let ii = 0; ii < sections.length; ii++) { |
|
if (itemIdx === -1 || itemIdx === sections[ii].data.length) { |
|
// We intend for there to be overflow by one on both ends of the list. |
|
// This will be for headers and footers. When returning a header or footer |
|
// item the section itself is the item. |
|
return sections[ii]; |
|
} else if (itemIdx < sections[ii].data.length) { |
|
// If we are in the bounds of the list's data then return the item. |
|
return sections[ii].data[itemIdx]; |
|
} else { |
|
itemIdx -= sections[ii].data.length + 2; // Add two for the header and footer |
|
} |
|
} |
|
return null; |
|
} |
|
|
|
module.exports = VirtualizedSectionList;
|
|
|