Merge branch 'dejal' of https://github.com/samuelclay/NewsBlur into dejal

# Conflicts:
#	clients/ios/Resources/MainInterface.storyboard
This commit is contained in:
David Sinclair 2024-05-13 19:34:09 -04:00
commit f77d8839ac
262 changed files with 53202 additions and 2321 deletions

4
.gitignore vendored
View file

@ -19,6 +19,7 @@ certbot.conf
task_env.py
app_env.py
data/
api/ip_addresses.txt
.prom_cache
config/certificates
**/*.xcuserstate
@ -53,10 +54,12 @@ docker/haproxy/haproxy.consul.cfg
docker/nginx/nginx.consul.conf
docker/prometheus/prometheus.yml
docker/redis/redis_replica.conf
docker/redis/redis_*_replica.conf
docker/postgres/postgres.conf
# Local configuration file (sdk path, etc)
/originals
/node/originals
media/safari/NewsBlur.safariextz
# IDE files
@ -66,3 +69,4 @@ media/safari/NewsBlur.safariextz
*.tfstate*
.terraform*
grafana.ini
apps/api/ip_addresses.txt

34
.vscode/settings.json vendored
View file

@ -1,23 +1,16 @@
{
"black-formatter.args": [
"--line-length 110"
],
"isort.args": [
"--profile",
"black"
],
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
},
"python.linting.enabled": true,
"python.linting.pylintEnabled": false,
"python.linting.flake8Enabled": true,
"python.linting.pylamaEnabled": false,
"python.linting.flake8Args": [
"--ignore=E501,W293,W503,W504,E302,E722,E226,E221,E402,E401"
],
"python.pythonPath": "~/.virtualenvs/newsblur3/bin/python",
// "python.linting.enabled": true,
// "python.linting.pylintEnabled": false,
// "python.linting.flake8Enabled": true,
// "python.linting.pylamaEnabled": false,
// "python.linting.flake8Args": [
// "--ignore=E501,W293,W503,W504,E302,E722,E226,E221,E402,E401"
// ],
// "python.pythonPath": "~/.virtualenvs/newsblur/bin/python",
"editor.bracketPairColorization.enabled": true,
"editor.guides.bracketPairs": "active",
"git.ignoreLimitWarning": true,
@ -38,15 +31,12 @@
"docker/volumes": true,
"requirements.txt": true, // It's just a symlink to config/requirements.txt, which has git history
},
"python.formatting.blackArgs": [
"--line-length=110",
"--skip-string-normalization"
],
// "python.formatting.blackArgs": [
// "--line-length=110",
// "--skip-string-normalization"
// ],
"files.associations": {
"*.yml": "ansible"
},
"nrf-connect.toolchain.path": "${nrf-connect.toolchain:1.9.1}",
"C_Cpp.default.configurationProvider": "nrf-connect",
"editor.formatOnSave": false,
"ansible.python.interpreterPath": "/opt/homebrew/bin/python3",
}

View file

@ -185,7 +185,7 @@ maintenance_off:
# Provision
firewall:
ansible-playbook ansible/all.yml -l db --tags firewall
ansible-playbook ansible/all.yml -l db --tags ufw
oldfirewall:
ANSIBLE_CONFIG=/srv/newsblur/ansible.old.cfg ansible-playbook ansible/all.yml -l db --tags firewall
repairmongo:

View file

@ -2,32 +2,48 @@ plugin: constructed
strict: False
groups:
hall: inventory_hostname.startswith('h')
haproxy: inventory_hostname.startswith('hwww')
web: inventory_hostname.startswith('happ')
app: inventory_hostname.startswith('happ')
django: inventory_hostname.startswith('happ-django')
happ: inventory_hostname.startswith('happ')
web: inventory_hostname.startswith('happ')
hweb: inventory_hostname.startswith('happ')
django: inventory_hostname.startswith('happ-web')
hdjango: inventory_hostname.startswith('happ-web')
refresh: inventory_hostname.startswith('happ-refresh')
counts: inventory_hostname.startswith('happ-counts')
hrefresh: inventory_hostname.startswith('happ-refresh')
count: inventory_hostname.startswith('happ-count')
hcount: inventory_hostname.startswith('happ-count')
push: inventory_hostname.startswith('happ-push')
hpush: inventory_hostname.startswith('happ-push')
blogs: inventory_hostname.startswith('blog')
node: inventory_hostname.startswith('hnode')
hnode: inventory_hostname.startswith('hnode')
node_socket: inventory_hostname.startswith('hnode-socket')
hnode_socket: inventory_hostname.startswith('hnode-socket')
node_images: inventory_hostname.startswith('hnode-images')
hnode_images: inventory_hostname.startswith('hnode-images')
node_text: inventory_hostname.startswith('hnode-text')
hnode_text: inventory_hostname.startswith('hnode-text')
node_page: inventory_hostname.startswith('hnode-page')
hnode_page: inventory_hostname.startswith('hnode-page')
node_favicons: inventory_hostname.startswith('hnode-favicons')
hnode_favicons: inventory_hostname.startswith('hnode-favicons')
# debugs: inventory_hostname.startswith('hdebug')
htask: inventory_hostname.startswith('htask')
task: inventory_hostname.startswith('htask')
celery: inventory_hostname.startswith('htask-celery')
work: inventory_hostname.startswith('htask-work')
staging: inventory_hostname.startswith('hstaging')
hdb: inventory_hostname.startswith('hdb')
db: inventory_hostname.startswith('hdb')
search: inventory_hostname.startswith('hdb-elasticsearch')
elasticsearch: inventory_hostname.startswith('hdb-elasticsearch')
@ -39,5 +55,6 @@ groups:
mongo: inventory_hostname.startswith('hdb-mongo') and not inventory_hostname.startswith('hdb-mongo-analytics')
mongo_analytics: inventory_hostname.startswith('hdb-mongo-analytics')
consul: inventory_hostname.startswith('hdb-consul')
hconsul: inventory_hostname.startswith('hdb-consul')
metrics: inventory_hostname.startswith('hdb-metrics')
sentry: inventory_hostname.startswith('hdb-sentry')

View file

@ -127,6 +127,9 @@
- name: Reload gunicorn
command: "kill -HUP {{ psaux.stdout }}"
when: not pulled.changed
rescue:
- name: Restart Docker Container
command: "docker restart newsblur_web"
tags:
- static

View file

@ -6,16 +6,23 @@
- ../env_vars/base.yml
tasks:
- name: Extract part of hostname to determine container name
set_fact:
redis_role: "{{ inventory_hostname.split('-')[2] }}"
tags:
- never
- replicaofnoone
- name: Turning off secondary for redis by deleting redis_replica.conf
copy:
dest: /srv/newsblur/docker/redis/redis_replica.conf
dest: "/srv/newsblur/docker/redis/redis_{{ redis_role }}_replica.conf"
content: ""
tags:
- never
- replicaofnoone
- name: Setting Redis REPLICAOF NO ONE
shell: docker exec redis redis-cli REPLICAOF NO ONE
shell: docker exec redis-{{ redis_role }} redis-cli REPLICAOF NO ONE
tags:
- never
- replicaofnoone

View file

@ -8,16 +8,16 @@
- motd_role: db
roles:
- {role: 'base', tags: 'base'}
- {role: 'ufw', tags: 'ufw'}
- {role: 'docker', tags: 'docker'}
- {role: 'repo', tags: ['repo', 'pull']}
- {role: 'dnsmasq', tags: 'dnsmasq'}
- {role: 'consul', tags: 'consul'}
- {role: 'consul-client', tags: 'consul'}
- {role: 'mongo-exporter', tags: 'mongo-exporter'}
- {role: 'postgres-exporter', tags: 'postgres-exporter'}
- {role: 'redis-exporter', tags: 'redis-exporter'}
- {role: 'node-exporter', tags: ['node-exporter', 'metrics']}
- {role: 'prometheus', tags: ['prometheus', 'metrics']}
- {role: 'grafana', tags: ['grafana', 'metrics']}
# - {role: 'base', tags: 'base'}
# - {role: 'ufw', tags: 'ufw'}
# - {role: 'docker', tags: 'docker'}
# - {role: 'repo', tags: ['repo', 'pull']}
# - {role: 'dnsmasq', tags: 'dnsmasq'}
# - {role: 'consul', tags: 'consul'}
# - {role: 'consul-client', tags: 'consul'}
# - {role: 'mongo-exporter', tags: 'mongo-exporter'}
- { role: "postgres-exporter", tags: "postgres-exporter" }
- { role: "redis-exporter", tags: "redis-exporter" }
- { role: "node-exporter", tags: ["node-exporter", "metrics"] }
- { role: "prometheus", tags: ["prometheus", "metrics"] }
- { role: "grafana", tags: ["grafana", "metrics"] }

View file

@ -1,7 +1,6 @@
---
- name: SETUP -> node containers
hosts: node
become: true
vars_files:
- ../env_vars/base.yml
vars:

View file

@ -12,8 +12,8 @@
- {role: 'docker', tags: 'docker'}
- {role: 'repo', tags: ['repo', 'pull']}
- {role: 'dnsmasq', tags: 'dnsmasq'}
- {role: 'consul', tags: 'consul'}
- {role: 'consul-client', tags: 'consul'}
# - {role: 'consul', tags: 'consul'}
# - {role: 'consul-client', tags: 'consul'}
- {role: 'node-exporter', tags: ['node-exporter', 'metrics']}
- {role: 'redis', tags: 'redis'}
- {role: 'flask_metrics', tags: ['flask-metrics', 'metrics', 'flask_metrics']}

View file

@ -23,5 +23,5 @@
- {role: 'nginx', tags: 'nginx'}
- {role: 'node', tags: 'node'}
- {role: 'node-exporter', tags: ['node-exporter', 'metrics']}
- {role: 'prometheus', tags: ['prometheus', 'metrics']}
- {role: 'grafana', tags: ['grafana', 'metrics']}
# - {role: 'prometheus', tags: ['prometheus', 'metrics']}
# - {role: 'grafana', tags: ['grafana', 'metrics']}

View file

@ -8,7 +8,6 @@
- motd_role: task
roles:
- {role: 'base', tags: 'base'}
# - {role: 'ufw', tags: 'ufw'}
- {role: 'docker', tags: 'docker'}
- {role: 'repo', tags: ['repo', 'pull']}
- {role: 'dnsmasq', tags: 'dnsmasq'}
@ -17,3 +16,4 @@
- {role: 'apns', tags: 'apns'}
- {role: 'node-exporter', tags: ['node-exporter', 'metrics']}
- {role: 'celery_task', tags: 'celery'}
- {role: 'ufw', tags: 'ufw'}

View file

@ -103,14 +103,15 @@
become: yes
command:
docker run --rm --name=pg_basebackup --network=host -e POSTGRES_PASSWORD=newsblur -v /srv/newsblur/docker/volumes/postgres/data:/var/lib/postgresql/data postgres:13 pg_basebackup -h db-postgres.service.nyc1.consul -p 5432 -U newsblur -D /var/lib/postgresql/data -Fp -R -Xs -P -c fast
- name: Create Postgres docker volumes with correct permissions
become: yes
file:
path: "{{ item }}"
path: "{{ item }}"
state: directory
recurse: yes
owner: "{{ ansible_effective_user_id|int }}"
group: "{{ ansible_effective_group_id|int }}"
owner: 999
group: 999
with_items:
- /srv/newsblur/docker/volumes/postgres/archive
- /srv/newsblur/docker/volumes/postgres/backups
@ -129,7 +130,7 @@
- name: pg_ctl promote
become: yes
command:
docker exec -it postgres su - postgres -c "/usr/lib/postgresql/13/bin/pg_ctl -D /var/lib/postgresql/data promote"
docker exec postgres su - postgres -c "/usr/lib/postgresql/13/bin/pg_ctl -D /var/lib/postgresql/data promote"
# when: (inventory_hostname | regex_replace('[0-9]+', '')) in ['db-postgres-secondary']
tags:
- never

View file

@ -6,6 +6,17 @@
tags: packages
# ignore_errors: yes
- name: whoami
debug:
var: ansible_user_id
tags: whoami
- name: Set timezone
become: yes
ansible.builtin.timezone:
name: 'America/New_York'
tags: timezone
- name: Copy zshrc
template:
src: zshrc.txt.j2
@ -20,10 +31,20 @@
become: yes
- name: Cloning oh-my-zsh
git:
repo: https://github.com/robbyrussell/oh-my-zsh
dest: /home/nb/.oh-my-zsh
force: yes
block:
- name: Cloning oh-my-zsh
git:
repo: https://github.com/robbyrussell/oh-my-zsh
dest: /home/nb/.oh-my-zsh
force: yes
rescue:
- name: chown oh-my-zsh
become: yes
file:
path: /home/nb/.oh-my-zsh
owner: nb
group: nb
recurse: yes
- name: Copy toprc
copy: src=toprc.txt dest=~/.toprc

View file

@ -43,7 +43,7 @@
max-size: 100m
healthcheck:
# test: celery inspect ping -A newsblur_web -d celery@$HOSTNAME
test: bash -c "(($(date +%s) - $(stat /srv/newsblur/logs/newsblur.log -c %Y) < 120)) && exit 0 || exit 1"
test: bash -c "(($(date +%s) - $(stat /srv/newsblur/logs/newsblur.log -c %Y) < 600)) && exit 0 || exit 1"
interval: 60s
timeout: 10s
retries: 3

View file

@ -9,7 +9,7 @@
"log_file": "/var/log/consul/consul.log",
"enable_syslog": true,
"retry_join": [{{ consul_manager_ip.stdout|trim }}],
{% if inventory_hostname.startswith("hdb") %}
{% if inventory_hostname.startswith("hdb") and inventory_hostname|regex_replace("\-?\d+", "") not in ["hdb-mongo-analytics", "hdb-sentry"] %}
"advertise_addr": "{% raw %}{{ GetAllInterfaces | include \"name\" \"^enp\" | include \"flags\" \"forwardable|up\" | attr \"address\" }}{% endraw %}",
{% else %}
"advertise_addr": "{% raw %}{{ GetAllInterfaces | include \"name\" \"^eth\" | include \"flags\" \"forwardable|up\" | attr \"address\" }}{% endraw %}",

View file

@ -1,12 +1,52 @@
#!/usr/bin/env python
import json
import os
import subprocess
import digitalocean
TOKEN_FILE = "/srv/secrets-newsblur/keys/digital_ocean.token"
OLD = False
# Uncomment below to allow existing servers to find the consul-manager
OLD = True
def get_host_ips_from_group(group_name):
"""
Fetches IP addresses of hosts from a specified group using ansible-inventory command across combined inventory.
:param group_name: The name of the group to fetch host IPs from.
:param inventory_base_path: Base path to the inventory directories. Defaults to the path in ansible.cfg.
:return: A list of IP addresses belonging to the specified group.
"""
cmd = ['ansible-inventory', '-i', '/srv/newsblur/ansible/inventories/hetzner.ini', '-i', '/srv/newsblur/ansible/inventories/hetzner.yml', '--list']
try:
# Execute the ansible-inventory command
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True)
# Parse the JSON output from ansible-inventory
inventory_data = json.loads(result.stdout)
host_ips = []
# Check if the group exists
if group_name in inventory_data:
# Get the list of hosts in the specified group
if 'hosts' in inventory_data[group_name]:
for host in inventory_data[group_name]['hosts']:
# Fetch the host details, specifically looking for the ansible_host variable for the IP
host_vars = inventory_data['_meta']['hostvars'][host]
ip_address = host_vars.get('ansible_host', None)
if ip_address:
host_ips.append(ip_address)
else:
# If ansible_host is not defined, fallback to using the host's name
host_ips.append(host)
return host_ips
except subprocess.CalledProcessError as e:
print(f"Failed to execute ansible-inventory: {e.stderr}")
return []
except json.JSONDecodeError as e:
print(f"Failed to parse JSON output: {e}")
return []
TOKEN_FILE = "/srv/secrets-newsblur/keys/digital_ocean.token"
with open(TOKEN_FILE) as f:
token = f.read().strip()
@ -14,11 +54,12 @@ with open(TOKEN_FILE) as f:
manager = digitalocean.Manager(token=token)
my_droplets = manager.get_all_droplets()
consul_manager_droplets = [d for d in my_droplets if d.name.startswith("db-consul")]
if OLD:
consul_manager_ip_address = ','.join([f"\"{droplet.ip_address}\"" for droplet in consul_manager_droplets])
else:
consul_manager_ip_address = ','.join([f"\"{droplet.private_ip_address}\"" for droplet in consul_manager_droplets])
consul_manager_droplets = [d for d in my_droplets if "db-consul" in d.name]
# Use ansible-inventory to get the consul-manager ip
group_name = 'hconsul'
hetzner_hosts = get_host_ips_from_group(group_name)
consul_manager_ip_address = ','.join([f"\"{droplet.ip_address}\"" for droplet in consul_manager_droplets] + [f"\"{host}\"" for host in hetzner_hosts])
print(consul_manager_ip_address)

View file

@ -12,11 +12,21 @@
apt_repository:
repo: "deb [arch=amd64] https://apt.releases.hashicorp.com {{ ansible_distribution_release }} main"
- name: Add the official HashiCorp Linux repository.
become: yes
apt_repository:
repo: "deb [arch=arm64] https://apt.releases.hashicorp.com {{ ansible_distribution_release }} main"
- name: Update apt
become: yes
apt:
update_cache: yes
- name: Installing Consul
become: yes
apt:
allow_downgrades: yes
pkg: consul=1.10.4
pkg: consul=1.10.12-1
state: present
- name: Register Manager IP

View file

@ -80,14 +80,20 @@
state: directory
mode: 0755
- name: Add override for dnsmasq service
become: yes
copy:
dest: /etc/systemd/system/dnsmasq.service.d/override.conf
content: |
[Unit]
After=docker.service
# - name: Add override for dnsmasq service
# become: yes
# copy:
# dest: /etc/systemd/system/dnsmasq.service.d/override.conf
# content: |
# [Unit]
# After=docker.service
- name: Remove override for dnsmasq service
become: yes
file:
path: /etc/systemd/system/dnsmasq.service.d/override.conf
state: absent
- name: Launch dnsmasq
become: yes
service:

View file

@ -9,12 +9,12 @@ server=/consul/127.0.0.1#8600
no-resolv
{% for interface in network_interfaces %}
{% if not interface.startswith('veth') %}
interface={{ interface }}
{% if not interface.startswith('veth') and not interface.startswith('docker') and not interface.startswith('br') %}
# interface={{ interface }}
{% endif %}
{% endfor %}
bind-interfaces
# bind-interfaces # This will bind only to the interfaces that are up in interface= above
# log-dhcp
# log-queries
# log-facility=/var/log/dnsmasq.log

View file

@ -15,25 +15,37 @@
state: present
with_items: "{{ docker_prerequisite_packages_Ubuntu }}"
- name: Install prerequisite packages (for Ubuntu 14.04 only)
apt:
name: "{{ item.package }}"
state: present
with_items: "{{ docker_prerequisite_packages_Ubuntu_1404 }}"
when: ansible_distribution_version == '14.04'
- name: Import Docker CE repository gpg key
- name: Download Docker GPG key
become: yes
apt_key:
url: https://download.docker.com/linux/ubuntu/gpg
state: present
id: 9DC858229FC7DD38854AE2D88D81803C0EBFCD88
- name: Add Docker CE repository
shell:
cmd: "curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg"
creates: /usr/share/keyrings/docker-archive-keyring.gpg
- name: Set up the Docker repository with the correct GPG key
become: yes
apt_repository:
repo: "deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
ansible.builtin.apt_repository:
repo: "deb [arch={{ ansible_architecture }} signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
state: present
update_cache: yes
- name: Set up the Docker repository with the correct GPG key
become: yes
ansible.builtin.apt_repository:
repo: "deb [arch=arm64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
state: present
update_cache: yes
- name: Set up the Docker repository with the correct GPG key
become: yes
ansible.builtin.apt_repository:
repo: "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
state: present
update_cache: yes
- name: Update APT cache
become: yes
ansible.builtin.apt:
update_cache: yes
- name: Install Docker CE
become: yes

View file

@ -1,4 +1,18 @@
---
- name: Set facts for secondary elasticsearch servers
set_fact:
elasticsearch_secondary: no
tags:
- always
- name: Set facts for secondary elasticsearch servers
set_fact:
elasticsearch_secondary: yes
# when: inventory_hostname not in ["db-elasticsearch"]
when: inventory_hostname not in ["hdb-elasticsearch-1"]
tags:
- always
- name: Permissions for elasticsearch
become: yes
file:

View file

@ -1,6 +1,6 @@
{
"service": {
{% if inventory_hostname in ["db-elasticsearch"] %}
{% if not elasticsearch_secondary %}
"name": "db-elasticsearch",
{% else %}
"name": "db-elasticsearch-staging",

View file

@ -1,4 +1,3 @@
- name: Start mongo-exporter container
become: yes
docker_container:
@ -12,9 +11,9 @@
- name: newsblurnet
env:
# MONGODB_URI: 'mongodb://{{ inventory_hostname }}.node.nyc1.consul:27017/admin?'
MONGODB_URI: 'mongodb://{{ mongodb_username }}:{{ mongodb_password }}@{{ inventory_hostname }}.node.nyc1.consul:27017/admin?authSource=admin'
MONGODB_URI: "mongodb://{{ mongodb_username }}:{{ mongodb_password }}@{{ inventory_hostname }}.node.nyc1.consul:27017/admin?authSource=admin"
ports:
- '9216:9216'
- "9216:9216"
- name: Register mongo-exporter in consul
tags: consul
@ -24,5 +23,5 @@
dest: /etc/consul.d/mongo-exporter.json
notify:
- reload consul
- name: Command to register mongo-exporter
command: "consul services register /etc/consul.d/mongo-exporter.json"
# - name: Command to register mongo-exporter
# command: "consul services register /etc/consul.d/mongo-exporter.json"

View file

@ -1,5 +1,22 @@
---
- name: Set mongo_analytics_secondary
set_fact:
mongo_analytics_secondary: no
tags:
- consul
- always
- name: Set mongo_analytics_secondary
set_fact:
mongo_analytics_secondary: yes
# when: (inventory_hostname | regex_replace('\-?[0-9]+', '')) not in ['db-mongo-analytics']
when: (inventory_hostname | regex_replace('\-?[0-9]+', '')) not in ['hdb-mongo-analytics']
tags:
- consul
- always
- name: Permissions for mongo
become: yes
file:
state: directory
mode: 0755
@ -8,7 +25,7 @@
path: /var/log/mongodb
- name: Copy MongoDB keyfile
# become: yes
become: yes
copy:
content: "{{ mongodb_keyfile }}"
dest: /srv/newsblur/config/mongodb_keyfile.key
@ -226,7 +243,6 @@
]
}
)' admin
when: (inventory_hostname | regex_replace('[0-9]+', '')) == 'db-mongo-analytics'
register: auth_result
changed_when:
- auth_result.rc == 0
@ -258,7 +274,7 @@
template:
src: consul_service.analytics.json
dest: /etc/consul.d/mongo.json
when: (inventory_hostname | regex_replace('[0-9]+', '')) == 'db-mongo-analytics'
when: (inventory_hostname | regex_replace('\-?[0-9]+', '')) in ['db-mongo-analytics', 'hdb-mongo-analytics']
notify:
- reload consul
@ -269,6 +285,7 @@
- logrotate
- name: Add sanity checkers cronjob for disk usage
become: yes
cron:
name: disk_usage_sanity_checker
user: root

View file

@ -1,6 +1,10 @@
{
"service": {
"name": "db-mongo-analytics",
{% if not mongo_analytics_secondary %}
"name": "db-mongo-analytics",
{% else %}
"name": "db-mongo-analytics-secondary",
{% endif %}
"id": "{{ inventory_hostname }}",
"tags": [
"db"

View file

@ -1,4 +1,13 @@
---
- name: Ensure 'nb' user owns /srv/ recursively
become: yes
file:
path: /srv
owner: nb
group: nb
recurse: yes
state: directory
- name: Copy node secrets
copy:
src: /srv/secrets-newsblur/settings/node_settings.env
@ -34,22 +43,22 @@
- name: Get the volume name
shell: ls /dev/disk/by-id/ | grep -v part
register: volume_name_raw
when: '"node-page" in inventory_hostname'
when: '"node-page" in inventory_hostname and not inventory_hostname.startswith("hnode")'
- set_fact:
volume_name: "{{ volume_name_raw.stdout }}"
when: '"node-page" in inventory_hostname'
when: '"node-page" in inventory_hostname and not inventory_hostname.startswith("hnode")'
- debug:
msg: "{{ volume_name }}"
when: '"node-page" in inventory_hostname'
when: '"node-page" in inventory_hostname and not inventory_hostname.startswith("hnode")'
- name: Create the mount point
become: yes
file:
path: "/mnt/{{ inventory_hostname | regex_replace('-', '') }}"
state: directory
when: '"node-page" in inventory_hostname'
when: '"node-page" in inventory_hostname and not inventory_hostname.startswith("hnode")'
- name: Mount volume read-write
become: yes
@ -59,7 +68,7 @@
fstype: xfs
opts: defaults,discard
state: mounted
when: '"node-page" in inventory_hostname'
when: '"node-page" in inventory_hostname and not inventory_hostname.startswith("hnode")'
- name: Symlink node-page volume from /srv/originals
become: yes
@ -67,10 +76,9 @@
dest: /srv/originals
src: "/mnt/{{ inventory_hostname | regex_replace('-', '') }}"
state: link
when: '"node-page" in inventory_hostname'
when: '"node-page" in inventory_hostname and not inventory_hostname.startswith("hnode")'
- name: Start node docker containers
become: yes
docker_container:
name: node
image: newsblur/newsblur_node
@ -104,7 +112,6 @@
when: item in inventory_hostname
- name: Start non-newsblur node docker containers
become: yes
docker_container:
name: "{{ item.container_name }}"
image: "{{ item.image }}"

View file

@ -1,6 +1,6 @@
{
"service": {
"name": "{{ inventory_hostname|regex_replace('\d+', '') }}",
"name": "{{ inventory_hostname|regex_replace('\-?\d+', '')|regex_replace('hnode', 'node') }}",
"id": "{{ inventory_hostname }}",
"tags": [
"web",

View file

@ -1,4 +1,18 @@
---
- name: Set facts for all postgres servers
set_fact:
postgres_secondary: no
tags:
- always
- name: Set facts for secondary postgres servers
set_fact:
postgres_secondary: yes
# when: inventory_hostname not in ["db-postgres2"]
when: inventory_hostname not in ["hdb-postgres-1"]
tags:
- always
- name: Template postgresql-13.conf file
template:
src: /srv/newsblur/docker/postgres/postgresql-13.conf.j2
@ -91,7 +105,7 @@
- /srv/newsblur/docker/postgres/postgres_ident-13.conf:/etc/postgresql/pg_ident.conf
- /home/nb/.ssh/id_rsa:/var/lib/postgresql/.ssh/id_rsa
restart_policy: unless-stopped
when: (inventory_hostname | regex_replace('\-?[0-9]+', '')) in ['db-postgres-primary', 'db-postgres', 'hdb-postgres']
when: (inventory_hostname | regex_replace('\-?[0-9]+', '')) in ['db-postgres-primary', 'db-postgres', 'hdb-postgres', 'hdb-postgres-secondary']
# - name: Change ownership in postgres docker container
# become: yes

View file

@ -1,6 +1,6 @@
{
"service": {
{% if inventory_hostname.startswith('db-postgres2') %}
{% if not postgres_secondary %}
"name": "db-postgres",
{% else %}
"name": "db-postgres-secondary",

View file

@ -6,7 +6,7 @@
restart_policy: unless-stopped
container_default_behavior: no_defaults
env:
REDIS_ADDR: "db-{{item.redis_target}}.service.nyc1.consul:6379"
REDIS_ADDR: "db-{{item.redis_target}}.service.nyc1.consul:{{ item.redis_port }}"
networks_cli_compatible: yes
network_mode: default
networks:
@ -16,12 +16,16 @@
with_items:
- port: 9121
redis_target: "redis-user"
redis_port: 6381
- port: 9122
redis_target: "redis-sessions"
redis_target: "redis-session"
redis_port: 6382
- port: 9123
redis_target: "redis-story"
redis_port: 6380
- port: 9124
redis_target: "redis-pubsub"
redis_port: 6383
- name: Register redis-exporters in consul
tags: consul
@ -35,7 +39,7 @@
- port: 9121
redis_target: "redis-user"
- port: 9122
redis_target: "redis-sessions"
redis_target: "redis-session"
- port: 9123
redis_target: "redis-story"
- port: 9124

View file

@ -8,7 +8,19 @@
state: reloaded
listen: reload consul
- name: restart redis
- name: restart redis user
become: yes
command: docker restart redis
listen: restart redis
command: docker restart redis-user
listen: restart redis_user
- name: restart redis story
become: yes
command: docker restart redis-story
listen: restart redis_story
- name: restart redis session
become: yes
command: docker restart redis-session
listen: restart redis_session
- name: restart redis pubsub
become: yes
command: docker restart redis-pubsub
listen: restart redis_pubsub

View file

@ -1,4 +1,31 @@
---
- name: Extract part of hostname to determine container name
set_fact:
redis_role: "{{ inventory_hostname.split('-')[2] }}"
redis_secondary: no
# redis_port: 6379
redis_ports:
story: 6380
user: 6381
session: 6382
pubsub: 6383
tags:
- always
- name: Set redis_port for redis servers
set_fact:
redis_port: "{{ redis_ports[redis_role] }}"
tags:
- always
- name: Set redis_secondary for secondary redis servers
set_fact:
redis_secondary: yes
# when: inventory_hostname not in ["db-redis-user", "db-redis-story1", "db-redis-session", "db-redis-pubsub"]
when: inventory_hostname not in ["hdb-redis-user-1", "hdb-redis-story-1", "hdb-redis-session-1", "hdb-redis-pubsub"]
tags:
- always
- name: Install sysfsutils for disabling transparent huge pages
become: yes
package:
@ -25,16 +52,22 @@
copy:
src: /srv/newsblur/docker/redis/redis.conf
dest: /srv/newsblur/docker/redis/redis.conf
notify: restart redis
notify: "restart redis_{{ redis_role }}"
register: updated_config
- name: Template redis_replica.conf file
template:
src: /srv/newsblur/docker/redis/redis_replica.conf.j2
dest: /srv/newsblur/docker/redis/redis_replica.conf
notify: restart redis
dest: "/srv/newsblur/docker/redis/redis_{{ redis_role }}_replica.conf"
notify: "restart redis_{{ redis_role }}"
register: updated_config
when: "'db-redis-story2' not in inventory_hostname"
when: redis_secondary
- name: Remove redis_replica.conf file if not secondary
copy:
dest: "/srv/newsblur/docker/redis/redis_{{ redis_role }}_replica.conf"
content: ""
when: not redis_secondary
- name: Create Redis docker volume directory
file:
@ -46,7 +79,7 @@
- name: Create Redis docker volume with correct permissions
file:
path: /srv/newsblur/docker/volumes/redis
path: "/srv/newsblur/docker/volumes/redis_{{ redis_role }}"
state: directory
recurse: yes
owner: "{{ ansible_effective_user_id|int }}"
@ -54,7 +87,7 @@
- name: Start redis docker containers
docker_container:
name: redis
name: "redis-{{ redis_role }}"
image: redis:7
pull: true
state: started
@ -69,19 +102,19 @@
aliases:
- redis
ports:
- 6379:6379
- "{{ redis_port }}:6379"
restart_policy: unless-stopped
user: "{{ ansible_effective_user_id|int }}:{{ ansible_effective_group_id|int }}"
volumes:
- /srv/newsblur/docker/volumes/redis:/data
- /srv/newsblur/docker/redis/redis.conf:/usr/local/etc/redis/redis_server.conf
- /srv/newsblur/docker/redis/redis_replica.conf:/usr/local/etc/redis/redis_replica.conf
- "/srv/newsblur/docker/volumes/redis_{{ redis_role }}:/data"
- "/srv/newsblur/docker/redis/redis.conf:/usr/local/etc/redis/redis_server.conf"
- "/srv/newsblur/docker/redis/redis_{{ redis_role }}_replica.conf:/usr/local/etc/redis/redis_replica.conf"
- name: Register redis in consul
become: yes
template:
src: consul_service.json
dest: /etc/consul.d/redis.json
dest: "/etc/consul.d/redis_{{ redis_role }}.json"
notify:
- reload consul
tags: consul
@ -130,7 +163,7 @@
job: >
docker run --rm
-v /srv/newsblur:/srv/newsblur
-v /srv/newsblur/docker/volumes/redis/dump.rdb:/data/dump.rdb
-v /srv/newsblur/docker/volumes/redis_{{ redis_role }}/dump.rdb:/data/dump.rdb
--network=newsblurnet
--hostname={{ ansible_hostname }}
newsblur/newsblur_python3 python /srv/newsblur/utils/backups/backup_redis.py

View file

@ -1,25 +1,25 @@
{
"service": {
{% if inventory_hostname in ["db-redis-user", "db-redis-story1", "db-redis-session", "db-redis-pubsub"] %}
"name": "{{ inventory_hostname|regex_replace('\d+', '') }}",
{% if not redis_secondary %}
"name": "{{ inventory_hostname|regex_replace('\-?\d+', '')|regex_replace("hdb", "db") }}",
{% else %}
"name": "{{ inventory_hostname|regex_replace('\-?\d+', '')|regex_replace('hdb-', 'db-') }}-staging",
"name": "db-redis-{{ redis_role }}-staging",
{% endif %}
"id": "{{ inventory_hostname }}",
"tags": [
"redis"
],
"port": 6379,
"port": {{ redis_port }},
"checks": [{
"id": "{{inventory_hostname}}-ping",
{% if 'db-redis-story' in inventory_hostname %}
"http": "http://{{ ansible_host }}:5579/db_check/redis_story?consul=1",
"http": "http://{{ ansible_host }}:5579/db_check/redis_story?consul=1&port={{ redis_port }}",
{% elif 'db-redis-user' in inventory_hostname %}
"http": "http://{{ ansible_host }}:5579/db_check/redis_user?consul=1",
"http": "http://{{ ansible_host }}:5579/db_check/redis_user?consul=1&port={{ redis_port }}",
{% elif 'db-redis-pubsub' in inventory_hostname %}
"http": "http://{{ ansible_host }}:5579/db_check/redis_pubsub?consul=1",
{% elif 'db-redis-sessions' in inventory_hostname %}
"http": "http://{{ ansible_host }}:5579/db_check/redis_sessions?consul=1",
"http": "http://{{ ansible_host }}:5579/db_check/redis_pubsub?consul=1&port={{ redis_port }}",
{% elif 'db-redis-session' in inventory_hostname %}
"http": "http://{{ ansible_host }}:5579/db_check/redis_sessions?consul=1&port={{ redis_port }}",
{% else %}
"http": "http://{{ ansible_host }}:5000/db_check/redis?consul=1",
{% endif %}

View file

@ -1,14 +1,23 @@
---
- name: Ensure /srv exists and is owned by user
become: yes
file:
path: /srv
state: directory
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: 0755
- name: Pull sentry self-hosted github
git:
repo: https://github.com/getsentry/self-hosted.git
dest: /srv/sentry/
version: master
version: 24.4.1
- name: Updating Sentry
command:
chdir: /srv/sentry/
cmd: ./install.sh
cmd: ./install.sh --no-report-self-hosted-issues
- name: docker-compuse up -d
command:
@ -24,4 +33,3 @@
notify:
- reload consul
when: disable_consul_services_ie_staging is not defined

View file

@ -1,6 +1,6 @@
{
"service": {
"name": "{{ inventory_hostname|regex_replace('\d+', '') }}",
"name": "{{ inventory_hostname|regex_replace('\-?\d+', '')|regex_replace("hdb-", "db-") }}",
"id": "{{ inventory_hostname }}",
"tags": [
"sentry"

View file

@ -1,13 +1,26 @@
---
- name: Stop ufw and delete all rules
become: yes
ufw: state=reset
tags: ufw
- name: Set firewall default policy
- name: Set hosts
set_fact:
hetzner_hosts: "{{ groups['hall'] | map('extract', hostvars, ['ansible_host']) }}"
do_hosts: "{{ groups['NewsBlur_Docker'] | map('extract', hostvars, ['ansible_host']) }}"
- name: Generate UFW batch script
become: yes
ufw: state=disabled policy=reject
tags: ufw
template:
src: ufw_rules.sh.j2
dest: /tmp/ufw_rules.sh
mode: '0755'
# - name: Stop ufw and delete all rules
# become: yes
# ufw: state=reset
# tags: ufw
# - name: Set firewall default policy
# become: yes
# ufw: state=disabled policy=reject
# tags: ufw
#
# - name: Set ufw policy to deny all incoming connections
# ufw: policy=deny direction=incoming
@ -16,81 +29,87 @@
# - name: Set ufw policy to allow all ougoing connections
# ufw: policy=allow direction=outgoing
# tags: ufw
- name: Execute UFW batch script
become: yes
command: /tmp/ufw_rules.sh
# - name: Allow ssh
# become: yes
# ufw: rule=allow port=22
# tags: ufw
# - name: Allow all access from RFC1918 networks to this host
# become: yes
# ufw:
# rule: allow
# src: '{{ item }}'
# with_items:
# - 10.0.0.0/8
# - 172.18.0.0/16
# - 172.17.0.0/16
# tags:
# - firewall
# - ufw
- name: Allow ssh
become: yes
ufw: rule=allow port=22
tags: ufw
# - name: Allow all access from Hetzner inventory hosts
# become: yes
# ufw:
# rule: allow
# src: '{{ item }}'
# with_items: "{{ groups['hall'] | map('extract', hostvars, ['ansible_host']) }}"
# tags:
# - firewall
# - ufw
# - hetzner_firewall
# - hfirewall
- name: Allow all access from RFC1918 networks to this host
become: yes
ufw:
rule: allow
src: '{{ item }}'
with_items:
- 10.0.0.0/8
- 172.18.0.0/16
- 172.17.0.0/16
tags:
- firewall
- ufw
- name: Allow all access from Hetzner inventory hosts
become: yes
ufw:
rule: allow
src: '{{ item }}'
with_items: "{{ groups['NewsBlur_Hetzner'] | map('extract', hostvars, ['ansible_host']) }}"
tags:
- firewall
- ufw
- hetzner_firewall
# - name: Allow all access from Hetzner inventory hosts with docker
# become: yes
# ufw:
# rule: allow
# route: yes
# src: '{{ item }}'
# with_items: "{{ groups['hall'] | map('extract', hostvars, ['ansible_host']) }}"
# tags:
# - firewall
# - ufw
# - hetzner_firewall
# - hfirewall
- name: Allow all access from Hetzner inventory hosts with docker
become: yes
ufw:
rule: allow
route: yes
src: '{{ item }}'
with_items: "{{ groups['NewsBlur_Hetzner'] | map('extract', hostvars, ['ansible_host']) }}"
tags:
- firewall
- ufw
- hetzner_firewall
# - name: Allow all access from inventory hosts old + new
# become: yes
# ufw:
# rule: allow
# src: '{{ item }}'
# with_items: "{{ groups['oldandnew'] | map('extract', hostvars, ['ansible_host']) }}"
# when: "'oldandnew' in groups"
# tags:
# - firewall
# - ufw
- name: Allow all access from inventory hosts old + new
become: yes
ufw:
rule: allow
src: '{{ item }}'
with_items: "{{ groups['oldandnew'] | map('extract', hostvars, ['ansible_host']) }}"
when: "'oldandnew' in groups"
tags:
- firewall
- ufw
# - name: Allow all access from inventory hosts
# become: yes
# ufw:
# rule: allow
# src: '{{ item }}'
# with_items: "{{ groups['NewsBlur_Docker'] | map('extract', hostvars, ['ansible_host']) }}"
# when: "'oldandnew' not in groups"
# tags:
# - firewall
# - ufw
- name: Allow all access from inventory hosts
become: yes
ufw:
rule: allow
src: '{{ item }}'
with_items: "{{ groups['NewsBlur_Docker'] | map('extract', hostvars, ['ansible_host']) }}"
when: "'oldandnew' not in groups"
tags:
- firewall
- ufw
- name: Allow all access from inventory hosts with docker
become: yes
ufw:
rule: allow
route: yes
src: '{{ item }}'
with_items: "{{ groups['NewsBlur_Docker'] | map('extract', hostvars, ['ansible_host']) }}"
when: "'oldandnew' not in groups"
tags:
- firewall
- ufw
# - name: Allow all access from inventory hosts with docker
# become: yes
# ufw:
# rule: allow
# route: yes
# src: '{{ item }}'
# with_items: "{{ groups['NewsBlur_Docker'] | map('extract', hostvars, ['ansible_host']) }}"
# when: "'oldandnew' not in groups"
# tags:
# - firewall
# - ufw
# Code from https://stackoverflow.com/a/51741599/8717: "What is the best practice of docker + ufw under Ubuntu"
- name: Solving UFW and Docker issues by adding ufw after.rules

View file

@ -0,0 +1,43 @@
#!/bin/bash
# Script to apply UFW rules incrementally with regex matching for `ufw status verbose` output
# Fetch the current UFW status once and store it
CURRENT_UFW_STATUS=$(ufw status verbose)
# Function to check and apply a rule with regex for forward rules
apply_rule() {
local rule="$1"
local rule_type="$2" # "IN" for incoming, "FWD" for forwarded (route) rules
local ip_address="$3"
# Construct the regex pattern based on rule type
local regex_pattern
if [ "$rule_type" == "FWD" ]; then
regex_pattern="ALLOW FWD\\s+$ip_address"
else
regex_pattern="ALLOW IN\\s+$ip_address"
fi
# Use grep with -P for Perl-compatible regex, and -q to quietly check for a match
if echo "$CURRENT_UFW_STATUS" | grep -Pq -- "$regex_pattern"; then
echo "Rule already exists: $regex_pattern"
else
echo "Applying rule: $rule"
ufw $rule
fi
}
# Apply rules
# Example for direct allow rules
apply_rule "allow 22" "IN" "22" # IP address parameter is not used for this rule
# Example for forwarded (route) rules
{% for host in hetzner_hosts %}
apply_rule "allow from {{ host }}" "IN" "{{ host }}"
apply_rule "route allow from {{ host }}" "FWD" "{{ host }}"
{% endfor %}
{% for host in do_hosts %}
apply_rule "allow from {{ host }}" "IN" "{{ host }}"
apply_rule "route allow from {{ host }}" "FWD" "{{ host }}"
{% endfor %}

View file

@ -29,6 +29,14 @@
tags:
- static
- name: "Write out ips from ansible inventory of all servers with app/node/www/task in name into addresses.txt file"
template:
src: ip_addresses.txt.j2
dest: /srv/newsblur/apps/api/ip_addresses.txt
tags:
- inventory
- name: Prune docker
become: yes
community.docker.docker_prune:

View file

@ -0,0 +1,5 @@
{%- for item in hostvars %}
{% if "happ" in item or "hnode" in item or "hwww" in item or "htask" in item %}
{{ hostvars[item].ansible_host }}
{% endif %}
{%- endfor %}

View file

@ -26,4 +26,4 @@
- import_playbook: playbooks/setup_metrics.yml
when: "'metrics' in inventory_hostname"
- import_playbook: playbooks/setup_sentry.yml
when: "'sentry' in inventory_hostname"
when: "'sentry' in inventory_hostname or 'metrics' in inventory_hostname"

View file

@ -1,25 +1,28 @@
import os
import base64
import urllib.parse
import datetime
import os
import urllib.parse
import lxml.html
from django import forms
from django.conf import settings
from django.http import HttpResponse
from django.shortcuts import render
from django.contrib.auth import login as login_user
from django.contrib.auth import logout as logout_user
from apps.reader.forms import SignupForm, LoginForm
from django.core.mail import mail_admins
from django.http import HttpResponse
from django.shortcuts import render
from apps.profile.models import Profile
from apps.social.models import MSocialProfile, MSharedStory, MSocialSubscription
from apps.rss_feeds.models import Feed, MStarredStoryCounts, MStarredStory
from apps.reader.forms import LoginForm, SignupForm
from apps.reader.models import RUserStory, UserSubscription, UserSubscriptionFolders
from apps.rss_feeds.models import Feed, MStarredStory, MStarredStoryCounts
from apps.rss_feeds.text_importer import TextImporter
from apps.reader.models import UserSubscription, UserSubscriptionFolders, RUserStory
from apps.social.models import MSharedStory, MSocialProfile, MSocialSubscription
from utils import json_functions as json
from utils import log as logging
from utils.feed_functions import relative_timesince
from utils.user_functions import ajax_login_required, get_user
from utils.view_functions import required_params
from utils.user_functions import get_user, ajax_login_required
@json.json_view
@ -511,8 +514,11 @@ def save_story(request, token=None):
return response
def ip_addresses(request):
import digitalocean
doapi = digitalocean.Manager(token=settings.DO_TOKEN_API_IPADDRESSES)
droplets = doapi.get_all_droplets()
addresses = '\n'.join([d.ip_address for d in droplets if any(name in d.name for name in ['task', 'staging', 'app', 'node'])])
# Read local file /srv/newsblur/apps/api/ip_addresses.txt and return that
with open('/srv/newsblur/apps/api/ip_addresses.txt', 'r') as f:
addresses = f.read()
if request.user.is_authenticated:
mail_admins(f"IP Addresses accessed from {request.META['REMOTE_ADDR']} by {request.user}", addresses)
return HttpResponse(addresses, content_type='text/plain')

View file

@ -24,7 +24,7 @@ from utils.view_functions import is_true
from utils.story_functions import truncate_chars
from utils import log as logging
from utils import mongoengine_fields
from apns2.errors import BadDeviceToken, Unregistered
from apns2.errors import BadDeviceToken, Unregistered, DeviceTokenNotForTopic
from apns2.client import APNsClient
from apns2.payload import Payload
from bs4 import BeautifulSoup
@ -306,14 +306,15 @@ class MUserFeedNotification(mongo.Document):
tokens = MUserNotificationTokens.get_tokens_for_user(self.user_id)
# To update APNS:
# 0. Upgrade to latest openssl: brew install openssl
# 1. Create certificate signing request in Keychain Access
# 2. Upload to https://developer.apple.com/account/resources/certificates/list
# 3. Download to secrets/certificates/ios/aps.cer
# 4. Open in Keychain Access:
# 4. Open in Keychain Access, Under "My Certificates":
# - export "Apple Push Service: com.newsblur.NewsBlur" as aps.p12 (or just use aps.cer in #5)
# - export private key as aps_key.p12 WITH A PASSPHRASE (removed later)
# 5. openssl x509 -in aps.cer -inform DER -out aps.pem -outform PEM
# 6. openssl pkcs12 -nocerts -out aps_key.pem -in aps_key.p12
# 6. openssl pkcs12 -in aps_key.p12 -out aps_key.pem -nodes -legacy
# 7. openssl rsa -out aps_key.noenc.pem -in aps_key.pem
# 7. cat aps.pem aps_key.noenc.pem > aps.p12.pem
# 8. Verify: openssl s_client -connect gateway.push.apple.com:2195 -cert aps.p12.pem
@ -348,7 +349,7 @@ class MUserFeedNotification(mongo.Document):
)
try:
apns.send_notification(token, payload, topic="com.newsblur.NewsBlur")
except (BadDeviceToken, Unregistered):
except (BadDeviceToken, Unregistered, DeviceTokenNotForTopic):
logging.user(user, '~BMiOS token expired: ~FR~SB%s' % (token[:50]))
else:
confirmed_ios_tokens.append(token)

View file

@ -1598,7 +1598,7 @@ class Profile(models.Model):
self.save()
delta = datetime.datetime.now() - self.last_seen_on
months_ago = delta.days / 30
months_ago = round(delta.days / 30)
user = self.user
data = dict(user=user, months_ago=months_ago)
text = render_to_string('mail/email_premium_expire_grace.txt', data)
@ -1627,7 +1627,7 @@ class Profile(models.Model):
return
delta = datetime.datetime.now() - self.last_seen_on
months_ago = delta.days / 30
months_ago = round(delta.days / 30)
user = self.user
data = dict(user=user, months_ago=months_ago)
text = render_to_string('mail/email_premium_expire.txt', data)

View file

@ -1,73 +1,114 @@
import base64
import concurrent
import datetime
import random
import re
import socket
import ssl
import time
import urllib.error
import urllib.parse
import urllib.request
import zlib
import redis
import requests
import random
import zlib
import concurrent
import re
import ssl
import socket
import base64
import urllib.request, urllib.error, urllib.parse
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from django.template.loader import render_to_string
from django.db import IntegrityError
from django.db.utils import DatabaseError
from django.db.models import Q
from django.views.decorators.cache import never_cache
from django.urls import reverse
from django.conf import settings
from django.contrib.auth import login as login_user
from django.contrib.auth import logout as logout_user
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden, Http404, UnreadablePostError
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.mail import EmailMultiAlternatives
from django.core.validators import validate_email
from django.contrib.sites.models import Site
from django.db import IntegrityError
from django.db.models import Q
from django.db.utils import DatabaseError
from django.http import (
Http404,
HttpResponse,
HttpResponseForbidden,
HttpResponseRedirect,
UnreadablePostError,
)
from django.shortcuts import get_object_or_404, render
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils import feedgenerator
from django.utils.encoding import smart_str
from mongoengine.queryset import OperationError
from mongoengine.queryset import NotUniqueError
from apps.recommendations.models import RecommendedFeed
from apps.analyzer.models import MClassifierTitle, MClassifierAuthor, MClassifierFeed, MClassifierTag
from apps.analyzer.models import apply_classifier_titles, apply_classifier_feeds
from apps.analyzer.models import apply_classifier_authors, apply_classifier_tags
from apps.analyzer.models import get_classifiers_for_user, sort_classifiers_by_feed
from apps.profile.models import Profile, MCustomStyling, MDashboardRiver
from apps.reader.models import UserSubscription, UserSubscriptionFolders, RUserStory, RUserUnreadStory, Feature
from apps.reader.forms import SignupForm, LoginForm, FeatureForm
from apps.rss_feeds.models import MFeedIcon, MStarredStoryCounts, MSavedSearch
from django.views.decorators.cache import never_cache
from mongoengine.queryset import NotUniqueError, OperationError
from apps.analyzer.models import (
MClassifierAuthor,
MClassifierFeed,
MClassifierTag,
MClassifierTitle,
apply_classifier_authors,
apply_classifier_feeds,
apply_classifier_tags,
apply_classifier_titles,
get_classifiers_for_user,
sort_classifiers_by_feed,
)
from apps.notifications.models import MUserFeedNotification
from apps.profile.models import MCustomStyling, MDashboardRiver, Profile
from apps.reader.forms import FeatureForm, LoginForm, SignupForm
from apps.reader.models import (
Feature,
RUserStory,
RUserUnreadStory,
UserSubscription,
UserSubscriptionFolders,
)
from apps.recommendations.models import RecommendedFeed
from apps.rss_feeds.models import MFeedIcon, MSavedSearch, MStarredStoryCounts
from apps.search.models import MUserSearch
from apps.statistics.models import MStatistics, MAnalyticsLoader
from apps.statistics.models import MAnalyticsLoader, MStatistics
from apps.statistics.rstats import RStats
# from apps.search.models import SearchStarredStory
try:
from apps.rss_feeds.models import Feed, MFeedPage, DuplicateFeed, MStory, MStarredStory
from apps.rss_feeds.models import (
DuplicateFeed,
Feed,
MFeedPage,
MStarredStory,
MStory,
)
except:
pass
from apps.social.models import MSharedStory, MSocialProfile, MSocialServices
from apps.social.models import MSocialSubscription, MActivity, MInteraction
from apps.categories.models import MCategory
from apps.social.views import load_social_page
from apps.rss_feeds.tasks import ScheduleImmediateFetches
from utils import json_functions as json
from utils.user_functions import get_user, ajax_login_required
from utils.user_functions import extract_user_agent
from utils.feed_functions import relative_timesince
from utils.story_functions import format_story_link_date__short
from utils.story_functions import format_story_link_date__long
from utils.story_functions import strip_tags
from utils import log as logging
from utils.view_functions import get_argument_or_404, render_to, is_true
from utils.view_functions import required_params
from utils.ratelimit import ratelimit
from vendor.timezones.utilities import localtime_for_timezone
import tweepy
from apps.categories.models import MCategory
from apps.rss_feeds.tasks import ScheduleImmediateFetches
from apps.social.models import (
MActivity,
MInteraction,
MSharedStory,
MSocialProfile,
MSocialServices,
MSocialSubscription,
)
from apps.social.views import load_social_page
from utils import json_functions as json
from utils import log as logging
from utils.feed_functions import relative_timesince
from utils.ratelimit import ratelimit
from utils.story_functions import (
format_story_link_date__long,
format_story_link_date__short,
strip_tags,
)
from utils.user_functions import ajax_login_required, extract_user_agent, get_user
from utils.view_functions import (
get_argument_or_404,
is_true,
render_to,
required_params,
)
from vendor.timezones.utilities import localtime_for_timezone
BANNED_URLS = [
"brentozar.com",
]
@ -75,8 +116,10 @@ ALLOWED_SUBDOMAINS = [
'dev',
'www',
'hwww',
'dwww',
# 'beta', # Comment to redirect beta -> www, uncomment to allow beta -> staging (+ dns changes)
'staging',
'hstaging',
'discovery',
'debug',
'debug3',
@ -679,7 +722,7 @@ def load_single_feed(request, feed_id):
if page > 400:
logging.user(request, "~BR~FK~SBOver page 400 on single feed: %s" % page)
assert False
raise Http404
if query:
if user.profile.is_premium:
@ -879,7 +922,6 @@ def load_feed_page(request, feed_id):
raise Http404
feed = Feed.get_by_id(feed_id)
if feed and feed.has_page and not feed.has_page_exception:
if settings.BACKED_BY_AWS.get('pages_on_node'):
domain = Site.objects.get_current().domain
@ -888,8 +930,9 @@ def load_feed_page(request, feed_id):
feed.pk,
)
try:
page_response = requests.get(url)
except requests.ConnectionError:
page_response = requests.get(url, verify=not settings.DEBUG)
except requests.ConnectionError as e:
logging.user(request, f"~FR~SBError loading original page: {url} {e}")
page_response = None
if page_response and page_response.status_code == 200:
response = HttpResponse(page_response.content, content_type="text/html; charset=utf-8")

View file

@ -1,33 +1,36 @@
import urllib.request
import base64
import datetime
import gzip
import http.client
import operator
import struct
import urllib.error
import urllib.parse
import urllib.request
from io import BytesIO
from socket import error as SocketError
import boto3
import lxml.html
import numpy
import scipy
import scipy.misc
import scipy.cluster
import struct
import operator
import gzip
import datetime
import requests
import base64
import http.client
from PIL import BmpImagePlugin, PngImagePlugin, Image
from socket import error as SocketError
import boto3
from io import BytesIO
import scipy
import scipy.cluster
import scipy.misc
from django.conf import settings
from django.http import HttpResponse
from django.contrib.sites.models import Site
from apps.rss_feeds.models import MFeedPage, MFeedIcon
from utils.facebook_fetcher import FacebookFetcher
from utils import log as logging
from utils.feed_functions import timelimit, TimeoutError
from OpenSSL.SSL import Error as OpenSSLError, SESS_CACHE_NO_INTERNAL_STORE
from django.http import HttpResponse
from OpenSSL.SSL import SESS_CACHE_NO_INTERNAL_STORE
from OpenSSL.SSL import Error as OpenSSLError
from PIL import BmpImagePlugin, Image, PngImagePlugin
from pyasn1.error import PyAsn1Error
from requests.packages.urllib3.exceptions import LocationParseError
from apps.rss_feeds.models import MFeedIcon, MFeedPage
from utils import log as logging
from utils.facebook_fetcher import FacebookFetcher
from utils.feed_functions import TimeoutError, timelimit
class IconImporter(object):
@ -206,8 +209,10 @@ class IconImporter(object):
if self.page_data:
content = self.page_data
elif settings.BACKED_BY_AWS.get('pages_on_node'):
domain = Site.objects.get_current().domain
url = "https://%s/original_page/%s" % (
domain = "node-page.service.consul:8008"
if settings.DOCKERBUILD:
domain = "node:8008"
url = "http://%s/original_page/%s" % (
domain,
self.feed.pk,
)

View file

@ -462,6 +462,10 @@ class Feed(models.Model):
username = re.search('youtube.com/user/(\w+)', url).group(1)
url = "http://gdata.youtube.com/feeds/base/users/%s/uploads" % username
without_rss = True
if url and 'youtube.com/@' in url:
username = url.split('youtube.com/@')[1]
url = "http://gdata.youtube.com/feeds/base/users/%s/uploads" % username
without_rss = True
if url and 'youtube.com/channel/' in url:
channel_id = re.search('youtube.com/channel/([-_\w]+)', url).group(1)
url = "https://www.youtube.com/feeds/videos.xml?channel_id=%s" % channel_id

View file

@ -1,23 +1,28 @@
import requests
import re
import traceback
import feedparser
import time
import urllib.request, urllib.error, urllib.parse
import http.client
import re
import time
import traceback
import urllib.error
import urllib.parse
import urllib.request
import zlib
from socket import error as SocketError
import feedparser
import requests
from django.conf import settings
from django.contrib.sites.models import Site
from django.utils.encoding import smart_bytes
from mongoengine.queryset import NotUniqueError
from socket import error as SocketError
from django.conf import settings
from django.utils.text import compress_string as compress_string_with_gzip
from utils import log as logging
from apps.rss_feeds.models import MFeedPage
from utils.feed_functions import timelimit, TimeoutError
from mongoengine.queryset import NotUniqueError
from OpenSSL.SSL import Error as OpenSSLError
from pyasn1.error import PyAsn1Error
from sentry_sdk import capture_exception, flush
from apps.rss_feeds.models import MFeedPage
from utils import log as logging
from utils.feed_functions import TimeoutError, timelimit
# from utils.feed_functions import mail_feed_error_to_admin
BROKEN_PAGES = [
@ -127,6 +132,7 @@ class PageImporter(object):
return
except (ValueError, urllib.error.URLError, http.client.BadStatusLine, http.client.InvalidURL,
requests.exceptions.ConnectionError) as e:
logging.debug(' ***> [%-30s] Page fetch failed: %s' % (self.feed.log_title[:30], e))
self.feed.save_page_history(401, "Bad URL", e)
try:
fp = feedparser.parse(self.feed.feed_address)
@ -134,10 +140,8 @@ class PageImporter(object):
return html
feed_link = fp.feed.get('link', "")
self.feed.save()
logging.debug(' ***> [%-30s] Page fetch failed: %s' % (self.feed.log_title[:30], e))
except (urllib.error.HTTPError) as e:
self.feed.save_page_history(e.code, e.msg, e.fp.read())
except (http.client.IncompleteRead) as e:
logging.debug(' ***> [%-30s] Page fetch failed: %s' % (self.feed.log_title[:30], e))
self.feed.save_page_history(500, "IncompleteRead", e)
except (requests.exceptions.RequestException,
requests.packages.urllib3.exceptions.HTTPError) as e:
@ -305,8 +309,10 @@ class PageImporter(object):
return feed_page
def save_page_node(self, html):
domain = Site.objects.get_current().domain
url = "https://%s/original_page/%s" % (
domain = "node-page.service.consul:8008"
if settings.DOCKERBUILD:
domain = "node:8008"
url = "http://%s/original_page/%s" % (
domain,
self.feed.pk,
)

View file

@ -2,13 +2,16 @@ import datetime
import os
import shutil
import time
import redis
from newsblur_web.celeryapp import app
from celery.exceptions import SoftTimeLimitExceeded
from utils import log as logging
from django.conf import settings
from apps.profile.middleware import DBProfilerMiddleware
from newsblur_web.celeryapp import app
from utils import log as logging
from utils.redis_raw_log_middleware import RedisDumpMiddleware
FEED_TASKING_MAX = 10000
@app.task(name='task-feeds')
@ -45,6 +48,7 @@ def TaskFeeds():
else:
logging.debug(" ---> ~SN~FBToo many tasked feeds. ~SB%s~SN tasked." % tasked_feeds_size)
active_count = 0
feeds = []
logging.debug(" ---> ~SN~FBTasking %s feeds took ~SB%s~SN seconds (~SB%s~SN/~FG%s~FB~SN/%s tasked/queued/scheduled)" % (
active_count,

View file

@ -0,0 +1,75 @@
Migration from Digital Ocean to Hetzner, covering about 120 servers (20 db, 80 task, 20 app)
## Tweet
> "Time for a big server transition. Im moving the servers from Digital Ocean to Hetzner. Every database server (postgresql, mongo, redis x 4, elasticsearch, prometheus, consul, and sentry) is making the move. Im going to do it all at once, which means about an hour of downtime."
> "Afterwards, you shouldn't notice anything different, although these are bare metal servers, so theoretically they should be faster and more reliable."
```
make maintenance_on
make celery_stop
```
## Postgres
> Edit postgres/consul_service.json: db-postgres2 -> hdb-postgres-1
```
aps -l db-postgres2,hdb-postgres-1,hdb-postgres-2 -t consul
aps -l hdb-postgres-1 -t pg_promote
```
## Mongo
```
sshdo db-mongo-primary1
sudo docker exec -it mongo mongo
rs.config()
rs.reconfig()
```
## Mongo analytics
> Edit mongo/tasks/main.yml: mongo_analytics_secondary
```
aps -l db-mongo-analytics2,hdb-mongo-analytics-1 -t consul
```
## Redis
> Edit redis/tasks/main.yml: redis_secondary
```
aps -l hdb-redis-user-1,hdb-redis-user-2,db-redis-user -t consul
aps -l hdb-redis-session-1,hdb-redis-session-2,db-redis-sessions -t consul
aps -l hdb-redis-story-1,hdb-redis-story-2,db-redis-story1 -t consul
aps -l hdb-redis-pubsub,db-redis-pubsub -t consul
apd -l hdb-redis-user-1,hdb-redis-session-1,hdb-redis-story-1,hdb-redis-pubsub -t replicaofnoone
```
## Elasticsearch
> Edit elasticsearch/tasks/main.yml: elasticsearch_secondary
```
aps -l db-elasticsearch1,hdb-elasticsearch-1 -t consul
```
> Eventually `MUserSearch.remove_all()`
## Test hwww.newsblur.com
```
ansible-playbook ansible/deploy.yml -l happ-web-01 --tags maintenance_off
```
## Looks good? Launch
> Haproxy on DO to redirect to Hetzner
```
aps -l www -t haproxy
```
> Change DNS to point to Hetzner
```
make maintenance_off
```

View file

@ -44,6 +44,6 @@ dependencies {
androidComponents {
beforeVariants(selector().all()) {
it.enabled = it.buildType == Const.benchmark
it.enable = it.buildType == Const.benchmark
}
}

View file

@ -46,11 +46,11 @@ android {
}
buildFeatures {
viewBinding = true
buildConfig = true
}
}
dependencies {
implementation(Dependencies.coreKtx)
implementation(Dependencies.fragment)
implementation(Dependencies.recyclerView)
implementation(Dependencies.swipeRefreshLayout)

View file

@ -19,11 +19,12 @@
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer
# Prevent R8 from leaving Data object members always null
-keepclassmembers, allowobfuscation class * {
# R8
-if class *
-keepclasseswithmembers, allowobfuscation class <1> {
<init>(...);
@com.google.gson.annotations.SerializedName <fields>;
}
# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher.
-keep, allowobfuscation, allowshrinking class com.google.gson.reflect.TypeToken
-keep, allowobfuscation, allowshrinking class * extends com.google.gson.reflect.TypeToken

View file

@ -1,5 +1,11 @@
package com.newsblur
import android.database.AbstractWindowedCursor
import android.database.Cursor
import android.database.CursorWindow
import android.database.sqlite.SQLiteBlobTooBigException
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteDatabase.OpenParams
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.gson.Gson
import com.google.gson.GsonBuilder
@ -12,8 +18,11 @@ import com.newsblur.serialization.ClassifierMapTypeAdapter
import com.newsblur.serialization.DateStringTypeAdapter
import com.newsblur.serialization.StoriesResponseTypeAdapter
import com.newsblur.serialization.StoryTypeAdapter
import org.junit.Assert.fail
import org.junit.Test
import org.junit.runner.RunWith
import java.lang.reflect.Field
import java.util.Arrays
import java.util.Date
@RunWith(AndroidJUnit4::class)
@ -32,4 +41,40 @@ class ParsingTest {
val input = """""".trimIndent()
}
/**
* https://android.googlesource.com/platform/cts/+/master/tests/tests/database/src/android/database/sqlite/cts/SQLiteCursorTest.java
* New cursor window constructor from API 28+
*/
@Test
fun testRowTooBig() {
val mDatabase = SQLiteDatabase.createInMemory(OpenParams.Builder().build())
mDatabase.execSQL("CREATE TABLE Tst (Txt BLOB NOT NULL);")
val testArr = ByteArray(10000)
Arrays.fill(testArr, 1.toByte())
for (i in 0..9) {
mDatabase.execSQL("INSERT INTO Tst VALUES (?)", arrayOf<Any>(testArr))
}
// Now reduce window size, so that no rows can fit
val cursor: Cursor = mDatabase.rawQuery("SELECT * FROM TST", null)
val cw = CursorWindow("test", 5000)
val ac = cursor as AbstractWindowedCursor
ac.window = cw
try {
ac.moveToNext()
fail("Exception is expected when row exceeds CursorWindow size")
} catch (expected: SQLiteBlobTooBigException) {
}
}
@Test
fun cursorWindowReflection() {
try {
val field: Field = CursorWindow::class.java.getDeclaredField("sCursorWindowSize")
field.isAccessible = true
field.set(null, 100 * 1024 * 1024) //the 100MB is the new size
} catch (e: Exception) {
e.printStackTrace()
}
}
}

View file

@ -155,6 +155,13 @@
</intent-filter>
</receiver>
<receiver android:name=".util.DownloadCompleteReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
</intent-filter>
</receiver>
<receiver android:name=".util.NotifyDismissReceiver" android:exported="false" />
<receiver android:name=".util.NotifySaveReceiver" android:exported="false" />
<receiver android:name=".util.NotifyMarkreadReceiver" android:exported="false" />

View file

@ -6,7 +6,6 @@ import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.DialogFragment;
import com.newsblur.R;
@ -27,11 +26,7 @@ public class AddFeedExternal extends NbActivity implements AddFeedFragment.AddFe
UIUtils.setupToolbar(this, R.drawable.logo, "Add Feed", true);
binding.loadingThrob.setEnabled(!ViewUtils.isPowerSaveMode(this));
binding.loadingThrob.setColors(ContextCompat.getColor(this, R.color.refresh_1),
ContextCompat.getColor(this, R.color.refresh_2),
ContextCompat.getColor(this, R.color.refresh_3),
ContextCompat.getColor(this, R.color.refresh_4));
binding.loadingIndicator.setEnabled(!ViewUtils.isPowerSaveMode(this));
Intent intent = getIntent();
Uri uri = intent.getData();
@ -53,7 +48,7 @@ public class AddFeedExternal extends NbActivity implements AddFeedFragment.AddFe
public void run() {
binding.progressText.setText(R.string.adding_feed_progress);
binding.progressText.setVisibility(View.VISIBLE);
binding.loadingThrob.setVisibility(View.VISIBLE);
binding.loadingIndicator.setVisibility(View.VISIBLE);
}
});
}

View file

@ -5,32 +5,43 @@ import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import com.google.android.material.snackbar.Snackbar
import com.newsblur.R
import com.newsblur.databinding.ActivityImportExportBinding
import com.newsblur.network.APIConstants
import com.newsblur.network.APIManager
import com.newsblur.service.NBSyncService
import com.newsblur.util.*
import com.newsblur.util.DownloadCompleteReceiver
import com.newsblur.util.FeedUtils
import com.newsblur.util.FileDownloader
import com.newsblur.util.NBScope
import com.newsblur.util.UIUtils
import com.newsblur.util.executeAsyncTask
import com.newsblur.util.setViewGone
import com.newsblur.util.setViewVisible
import dagger.hilt.android.AndroidEntryPoint
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File
import javax.inject.Inject
@AndroidEntryPoint
class ImportExportActivity : NbActivity() {
@Inject
lateinit var apiManager: APIManager
private val pickXmlFileRequestCode = 10
private lateinit var binding: ActivityImportExportBinding
override fun onCreate(bundle: Bundle?) {
super.onCreate(bundle)
private val filePickResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
handleFilePickResult(result.data)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityImportExportBinding.inflate(layoutInflater)
setContentView(binding.root)
@ -53,13 +64,14 @@ class ImportExportActivity : NbActivity() {
addCategory(Intent.CATEGORY_OPENABLE)
type = mineType
}.also {
startActivityForResult(it, pickXmlFileRequestCode)
filePickResultLauncher.launch(it)
}
}
private fun exportOpmlFile() {
val exportOpmlUrl = APIConstants.buildUrl(APIConstants.PATH_EXPORT_OPML)
UIUtils.handleUri(this, Uri.parse(exportOpmlUrl))
DownloadCompleteReceiver.expectedFileDownloadId = FileDownloader.exportOpml(this)
val msg = "${getString(R.string.newsblur_opml)} download started"
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}
private fun importOpmlFile(uri: Uri) {
@ -106,18 +118,14 @@ class ImportExportActivity : NbActivity() {
)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
super.onActivityResult(requestCode, resultCode, resultData)
if (requestCode == pickXmlFileRequestCode && resultCode == Activity.RESULT_OK) {
resultData?.data?.also { uri ->
importOpmlFile(uri)
}
?: Snackbar.make(
binding.root,
"OPML file retrieval failed!",
Snackbar.LENGTH_LONG
).show()
}
private fun handleFilePickResult(resultData: Intent?) {
resultData?.data?.also { uri ->
importOpmlFile(uri)
} ?: Snackbar.make(
binding.root,
"OPML file retrieval failed!",
Snackbar.LENGTH_LONG
).show()
}
override fun handleUpdate(updateType: Int) {

View file

@ -1,8 +1,8 @@
package com.newsblur.activity;
import static com.newsblur.service.NBSyncReceiver.UPDATE_REBUILD;
import static com.newsblur.service.NBSyncReceiver.UPDATE_STATUS;
import static com.newsblur.service.NBSyncReceiver.UPDATE_STORY;
import static com.newsblur.service.NbSyncManager.UPDATE_REBUILD;
import static com.newsblur.service.NbSyncManager.UPDATE_STATUS;
import static com.newsblur.service.NbSyncManager.UPDATE_STORY;
import android.content.Intent;
import android.os.Bundle;

View file

@ -70,7 +70,7 @@ class LoginProgress : FragmentActivity() {
val startMain = Intent(this, Main::class.java)
startActivity(startMain)
} else {
UIUtils.safeToast(this, it.getErrorMessage(getString(R.string.login_message_error)), Toast.LENGTH_LONG)
Toast.makeText(this, it.getErrorMessage(getString(R.string.login_message_error)), Toast.LENGTH_LONG).show()
startActivity(Intent(this, Login::class.java))
}
}

View file

@ -1,9 +1,9 @@
package com.newsblur.activity;
import static com.newsblur.service.NBSyncReceiver.UPDATE_DB_READY;
import static com.newsblur.service.NBSyncReceiver.UPDATE_METADATA;
import static com.newsblur.service.NBSyncReceiver.UPDATE_REBUILD;
import static com.newsblur.service.NBSyncReceiver.UPDATE_STATUS;
import static com.newsblur.service.NbSyncManager.UPDATE_DB_READY;
import static com.newsblur.service.NbSyncManager.UPDATE_METADATA;
import static com.newsblur.service.NbSyncManager.UPDATE_REBUILD;
import static com.newsblur.service.NbSyncManager.UPDATE_STATUS;
import android.content.Intent;
import android.graphics.Bitmap;

View file

@ -1,6 +1,6 @@
package com.newsblur.activity;
import static com.newsblur.service.NBSyncReceiver.UPDATE_STATUS;
import static com.newsblur.service.NbSyncManager.UPDATE_STATUS;
import android.content.Intent;
import android.database.Cursor;

View file

@ -1,17 +1,24 @@
package com.newsblur.activity
import android.content.IntentFilter
import androidx.appcompat.app.AppCompatActivity
import com.newsblur.util.PrefConstants.ThemeValue
import android.os.Bundle
import androidx.core.content.ContextCompat
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.newsblur.database.BlurDatabaseHelper
import com.newsblur.util.PrefsUtils
import com.newsblur.util.UIUtils
import com.newsblur.service.NBSyncReceiver
import com.newsblur.service.NBSync
import com.newsblur.service.NbSyncManager
import com.newsblur.util.FeedUtils
import com.newsblur.util.Log
import com.newsblur.util.PrefConstants.ThemeValue
import com.newsblur.util.PrefsUtils
import com.newsblur.util.UIUtils
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
/**
@ -27,14 +34,7 @@ open class NbActivity : AppCompatActivity() {
private var uniqueLoginKey: String? = null
private var lastTheme: ThemeValue? = null
// Facilitates the db updates by the sync service on the UI
private val serviceSyncReceiver = object : NBSyncReceiver() {
override fun handleUpdateType(updateType: Int) {
runOnUiThread { handleUpdate(updateType) }
}
}
override fun onCreate(bundle: Bundle?) {
override fun onCreate(savedInstanceState: Bundle?) {
Log.offerContext(this)
Log.d(this, "onCreate")
@ -43,7 +43,7 @@ open class NbActivity : AppCompatActivity() {
PrefsUtils.applyThemePreference(this)
lastTheme = PrefsUtils.getSelectedTheme(this)
super.onCreate(bundle)
super.onCreate(savedInstanceState)
// in rare cases of process interruption or DB corruption, an activity can launch without valid
// login creds. redirect the user back to the loging workflow.
@ -53,7 +53,7 @@ open class NbActivity : AppCompatActivity() {
finish()
}
bundle?.let {
savedInstanceState?.let {
uniqueLoginKey = it.getString(UNIQUE_LOGIN_KEY)
}
@ -62,6 +62,19 @@ open class NbActivity : AppCompatActivity() {
}
finishIfNotLoggedIn()
// Facilitates the db updates by the sync service on the UI
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
launch {
NbSyncManager.state.collectLatest {
withContext(Dispatchers.Main) {
handleSyncUpdate(it)
}
}
}
}
}
}
override fun onResume() {
@ -76,16 +89,11 @@ open class NbActivity : AppCompatActivity() {
PrefsUtils.applyThemePreference(this)
UIUtils.restartActivity(this)
}
ContextCompat.registerReceiver(this, serviceSyncReceiver, IntentFilter().apply {
addAction(NBSyncReceiver.NB_SYNC_ACTION)
}, ContextCompat.RECEIVER_NOT_EXPORTED)
}
override fun onPause() {
Log.d(this.javaClass.name, "onPause")
super.onPause()
unregisterReceiver(serviceSyncReceiver)
}
private fun finishIfNotLoggedIn() {
@ -109,15 +117,18 @@ open class NbActivity : AppCompatActivity() {
FeedUtils.triggerSync(this)
}
/**
* Called on each NB activity after the DB has been updated by the sync service.
*
* @param updateType one or more of the UPDATE_* flags in this class to indicate the
* type of update being broadcast.
*/
private fun handleSyncUpdate(nbSync: NBSync) = when (nbSync) {
is NBSync.Update -> handleUpdate(nbSync.type)
is NBSync.Error -> handleErrorMsg(nbSync.msg)
}
protected open fun handleUpdate(updateType: Int) {
Log.w(this, "activity doesn't implement handleUpdate")
}
private fun handleErrorMsg(msg: String) {
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}
}
private const val UNIQUE_LOGIN_KEY = "uniqueLoginKey"

View file

@ -27,8 +27,8 @@ class NotificationsActivity : NbActivity(), NotificationsAdapter.Listener {
private lateinit var viewModel: NotificationsViewModel
private lateinit var adapter: NotificationsAdapter
override fun onCreate(bundle: Bundle?) {
super.onCreate(bundle)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this)[NotificationsViewModel::class.java]
binding = ActivityNotificationsBinding.inflate(layoutInflater)
setContentView(binding.root)

View file

@ -60,9 +60,9 @@ class Profile : NbActivity() {
loadUserDetails()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
userId?.let { outState.putString(USER_ID, it) }
override fun onSaveInstanceState(savedInstanceState: Bundle) {
super.onSaveInstanceState(savedInstanceState)
userId?.let { savedInstanceState.putString(USER_ID, it) }
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {

View file

@ -30,9 +30,9 @@ import com.newsblur.fragment.ReadingPagerFragment
import com.newsblur.keyboard.KeyboardEvent
import com.newsblur.keyboard.KeyboardListener
import com.newsblur.keyboard.KeyboardManager
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_REBUILD
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_STATUS
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_STORY
import com.newsblur.service.NbSyncManager.UPDATE_REBUILD
import com.newsblur.service.NbSyncManager.UPDATE_STATUS
import com.newsblur.service.NbSyncManager.UPDATE_STORY
import com.newsblur.service.NBSyncService
import com.newsblur.util.AppConstants
import com.newsblur.util.CursorFilters
@ -116,7 +116,7 @@ abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListene
override fun onCreate(savedInstanceBundle: Bundle?) {
super.onCreate(savedInstanceBundle)
storiesViewModel = ViewModelProvider(this).get(StoriesViewModel::class.java)
storiesViewModel = ViewModelProvider(this)[StoriesViewModel::class.java]
binding = ActivityReadingBinding.inflate(layoutInflater)
setContentView(binding.root)
@ -159,20 +159,20 @@ abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListene
getActiveStoriesCursor(this, true)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
override fun onSaveInstanceState(savedInstanceState: Bundle) {
super.onSaveInstanceState(savedInstanceState)
if (storyHash != null) {
outState.putString(EXTRA_STORY_HASH, storyHash)
savedInstanceState.putString(EXTRA_STORY_HASH, storyHash)
} else if (pager != null) {
val currentItem = pager!!.currentItem
val story = readingAdapter!!.getStory(currentItem)
if (story != null) {
outState.putString(EXTRA_STORY_HASH, story.storyHash)
savedInstanceState.putString(EXTRA_STORY_HASH, story.storyHash)
}
}
if (startingUnreadCount != 0) {
outState.putInt(BUNDLE_STARTING_UNREAD, startingUnreadCount)
savedInstanceState.putInt(BUNDLE_STARTING_UNREAD, startingUnreadCount)
}
}

View file

@ -60,8 +60,8 @@ class SubscriptionActivity : NbActivity() {
}
}
override fun onCreate(bundle: Bundle?) {
super.onCreate(bundle)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySubscriptionBinding.inflate(layoutInflater)
bindingPremium = ViewPremiumSubscriptionBinding.bind(binding.containerPremiumSubscription.root)
bindingArchive = ViewArchiveSubscriptionBinding.bind(binding.containerArchiveSubscription.root)

View file

@ -15,15 +15,13 @@ import com.newsblur.domain.Story
import com.newsblur.fragment.LoadingFragment
import com.newsblur.fragment.ReadingItemFragment
import com.newsblur.fragment.ReadingItemFragment.Companion.newInstance
import com.newsblur.service.NBSyncReceiver
import com.newsblur.service.NbSyncManager
import com.newsblur.util.Log
import com.newsblur.util.NBScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
/**
* An adapter to display stories in a ViewPager. Loosely based upon FragmentStatePagerAdapter, but
@ -228,7 +226,7 @@ class ReadingAdapter(
for (s in stories) {
fragments[s.storyHash]?.let { rif ->
rif.offerStoryUpdate(s)
rif.handleUpdate(NBSyncReceiver.UPDATE_STORY)
rif.handleUpdate(NbSyncManager.UPDATE_STORY)
}
}
}

View file

@ -24,9 +24,12 @@ import android.widget.RelativeLayout;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import com.newsblur.R;
import com.newsblur.activity.FeedItemsList;
@ -43,6 +46,8 @@ import com.newsblur.util.PrefsUtils;
import com.newsblur.util.SpacingStyle;
import com.newsblur.util.StoryContentPreviewStyle;
import com.newsblur.util.StoryListStyle;
import com.newsblur.util.StoryOrder;
import com.newsblur.util.StoryUtil;
import com.newsblur.util.StoryUtils;
import com.newsblur.util.ThumbnailStyle;
import com.newsblur.util.UIUtils;
@ -90,6 +95,7 @@ public class StoryViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
private final UserDetails user;
private ThumbnailStyle thumbnailStyle;
private SpacingStyle spacingStyle;
private StoryOrder storyOrder;
public StoryViewAdapter(NbActivity context,
ItemSetFragment fragment,
@ -123,6 +129,7 @@ public class StoryViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
user = PrefsUtils.getUserDetails(context);
thumbnailStyle = PrefsUtils.getThumbnailStyle(context);
spacingStyle = PrefsUtils.getSpacingStyle(context);
storyOrder = PrefsUtils.getStoryOrder(context, fs);
executorService = Executors.newFixedThreadPool(1);
@ -195,12 +202,12 @@ public class StoryViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
if (position >= getStoryCount()) {
return (footerViews.get(position - getStoryCount()).hashCode());
}
if (position >= stories.size() || position < 0) return 0;
return stories.get(position).storyHash.hashCode();
}
public void swapCursor(final Cursor c, final RecyclerView rv, Parcelable oldScrollState) {
public void swapCursor(final Cursor c, final RecyclerView rv, Parcelable oldScrollState, final boolean skipBackFillingStories) {
// cache the identity of the most recent cursor so async batches can check to
// see if they are stale
cursor = c;
@ -213,7 +220,7 @@ public class StoryViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
Runnable r = new Runnable() {
@Override
public void run() {
thawDiffUpdate(c, rv);
thawDiffUpdate(c, rv, skipBackFillingStories);
}
};
executorService.submit(r);
@ -223,7 +230,7 @@ public class StoryViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
* Attempt to thaw a new set of stories from the cursor most recently
* seen when the that cycle started.
*/
private void thawDiffUpdate(final Cursor c, final RecyclerView rv) {
private void thawDiffUpdate(final Cursor c, final RecyclerView rv, final boolean skipBackFillingStories) {
if (c != cursor) return;
// thawed stories
@ -234,14 +241,42 @@ public class StoryViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
// as a new one will be provided and another cycle will start. just return.
try {
if (c == null) {
newStories = new ArrayList<Story>(0);
newStories = new ArrayList<>();
} else {
if (c.isClosed()) return;
newStories = new ArrayList<Story>(c.getCount());
newStories = new ArrayList<>(c.getCount());
c.moveToPosition(-1);
// The 'skipBackFillingStories' flag is used to ensure that when the adapter resumes,
// it omits any new stories that would disrupt the current order and cause the list to
// unexpectedly jump, thereby preserving the scroll position. This flag specifically helps
// manage the insertion of new stories that have been backfilled according to their timestamps.
Set<String> currentStoryHashes = skipBackFillingStories ?
StoryUtil.getStoryHashes(stories) : Collections.emptySet();
Long storyTimestampThreshold;
if (skipBackFillingStories && storyOrder == StoryOrder.NEWEST) {
storyTimestampThreshold = StoryUtil.getOldestStoryTimestamp(stories);
} else if (skipBackFillingStories && storyOrder == StoryOrder.OLDEST) {
storyTimestampThreshold = StoryUtil.getNewestStoryTimestamp(stories);
} else {
storyTimestampThreshold = null;
}
while (c.moveToNext()) {
if (c.isClosed()) return;
Story s = Story.fromCursor(c);
if (skipBackFillingStories && !currentStoryHashes.contains(s.storyHash)) {
if (storyOrder == StoryOrder.NEWEST &&
storyTimestampThreshold != null &&
s.timestamp >= storyTimestampThreshold) {
continue;
} else if (storyOrder == StoryOrder.OLDEST &&
storyTimestampThreshold != null &&
s.timestamp <= storyTimestampThreshold) {
continue;
}
}
s.bindExternValues(c);
newStories.add(s);
if (! s.read) indexOfLastUnread = c.getPosition();
@ -336,8 +371,8 @@ public class StoryViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
}
public class StoryViewHolder extends RecyclerView.ViewHolder
implements View.OnClickListener,
View.OnCreateContextMenuListener,
implements View.OnClickListener,
View.OnCreateContextMenuListener,
MenuItem.OnMenuItemClickListener,
View.OnTouchListener {

View file

@ -8,6 +8,7 @@ import android.text.TextUtils;
import com.google.gson.annotations.SerializedName;
import com.newsblur.database.DatabaseConstants;
import com.newsblur.util.StoryUtil;
public class Comment implements Serializable {
@ -55,7 +56,7 @@ public class Comment implements Serializable {
ContentValues values = new ContentValues();
values.put(DatabaseConstants.COMMENT_DATE, date);
values.put(DatabaseConstants.COMMENT_STORYID, storyId);
values.put(DatabaseConstants.COMMENT_LIKING_USERS, TextUtils.join(",", likingUsers));
values.put(DatabaseConstants.COMMENT_LIKING_USERS, StoryUtil.nullSafeJoin(",", likingUsers));
values.put(DatabaseConstants.COMMENT_TEXT, commentText);
values.put(DatabaseConstants.COMMENT_SHAREDDATE, sharedDate);
values.put(DatabaseConstants.COMMENT_BYFRIEND, byFriend ? "true" : "false");

View file

@ -16,6 +16,7 @@ import com.google.gson.annotations.SerializedName;
import com.newsblur.database.DatabaseConstants;
import com.newsblur.util.StateFilter;
import com.newsblur.util.StoryUtil;
public class Story implements Serializable {
@ -139,21 +140,21 @@ public class Story implements Serializable {
values.put(DatabaseConstants.STORY_AUTHORS, authors);
values.put(DatabaseConstants.STORY_SOCIAL_USER_ID, socialUserId);
values.put(DatabaseConstants.STORY_SOURCE_USER_ID, sourceUserId);
values.put(DatabaseConstants.STORY_SHARED_USER_IDS, TextUtils.join(",", sharedUserIds));
values.put(DatabaseConstants.STORY_FRIEND_USER_IDS, TextUtils.join(",", friendUserIds));
values.put(DatabaseConstants.STORY_SHARED_USER_IDS, StoryUtil.nullSafeJoin(",", sharedUserIds));
values.put(DatabaseConstants.STORY_FRIEND_USER_IDS, StoryUtil.nullSafeJoin(",", friendUserIds));
values.put(DatabaseConstants.STORY_INTELLIGENCE_AUTHORS, intelligence.intelligenceAuthors);
values.put(DatabaseConstants.STORY_INTELLIGENCE_FEED, intelligence.intelligenceFeed);
values.put(DatabaseConstants.STORY_INTELLIGENCE_TAGS, intelligence.intelligenceTags);
values.put(DatabaseConstants.STORY_INTELLIGENCE_TITLE, intelligence.intelligenceTitle);
values.put(DatabaseConstants.STORY_INTELLIGENCE_TOTAL, intelligence.calcTotalIntel());
values.put(DatabaseConstants.STORY_TAGS, TextUtils.join(",", tags));
values.put(DatabaseConstants.STORY_USER_TAGS, TextUtils.join(",", userTags));
values.put(DatabaseConstants.STORY_TAGS, StoryUtil.nullSafeJoin(",", tags));
values.put(DatabaseConstants.STORY_USER_TAGS, StoryUtil.nullSafeJoin(",", userTags));
values.put(DatabaseConstants.STORY_READ, read);
values.put(DatabaseConstants.STORY_STARRED, starred);
values.put(DatabaseConstants.STORY_STARRED_DATE, starredTimestamp);
values.put(DatabaseConstants.STORY_FEED_ID, feedId);
values.put(DatabaseConstants.STORY_HASH, storyHash);
values.put(DatabaseConstants.STORY_IMAGE_URLS, TextUtils.join(",", imageUrls));
values.put(DatabaseConstants.STORY_IMAGE_URLS, StoryUtil.nullSafeJoin(",", imageUrls));
values.put(DatabaseConstants.STORY_LAST_READ_DATE, lastReadTimestamp);
values.put(DatabaseConstants.STORY_SHARED_DATE, sharedTimestamp);
values.put(DatabaseConstants.STORY_SEARCH_HIT, searchHit);
@ -299,11 +300,11 @@ public class Story implements Serializable {
return true;
}
private static final Pattern ytSniff1 = Pattern.compile("youtube\\.com\\/embed\\/([A-Za-z0-9_-]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern ytSniff2 = Pattern.compile("youtube\\.com\\/v\\/([A-Za-z0-9_-]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern ytSniff3 = Pattern.compile("ytimg\\.com\\/vi\\/([A-Za-z0-9_-]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern ytSniff4 = Pattern.compile("youtube\\.com\\/watch\\?v=([A-Za-z0-9_-]+)", Pattern.CASE_INSENSITIVE);
private static final String YT_THUMB_PRE = "http://img.youtube.com/vi/";
private static final Pattern ytSniff1 = Pattern.compile("youtube\\.com/embed/([A-Za-z0-9_-]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern ytSniff2 = Pattern.compile("youtube\\.com/v/([A-Za-z0-9_-]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern ytSniff3 = Pattern.compile("ytimg\\.com/vi/([A-Za-z0-9_-]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern ytSniff4 = Pattern.compile("youtube\\.com/watch\\?v=([A-Za-z0-9_-]+)", Pattern.CASE_INSENSITIVE);
private static final String YT_THUMB_PRE = "https://img.youtube.com/vi/";
private static final String YT_THUMB_POST = "/0.jpg";
public static String guessStoryThumbnailURL(Story story) {

View file

@ -5,7 +5,6 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
@ -13,7 +12,6 @@ import android.os.Bundle;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
@ -67,12 +65,10 @@ public class ChooseFoldersFragment extends DialogFragment {
}
}
final Activity activity = getActivity();
LayoutInflater inflater = LayoutInflater.from(activity);
View v = inflater.inflate(R.layout.dialog_choosefolders, null);
View v = getLayoutInflater().inflate(R.layout.dialog_choosefolders, null);
DialogChoosefoldersBinding binding = DialogChoosefoldersBinding.bind(v);
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
builder.setTitle(String.format(getResources().getString(R.string.title_choose_folders), feed.title));
builder.setView(v);
@ -85,16 +81,16 @@ public class ChooseFoldersFragment extends DialogFragment {
builder.setPositiveButton(R.string.dialog_folders_save, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
feedUtils.moveFeedToFolders(activity, feed.feedId, newFolders, oldFolders);
feedUtils.moveFeedToFolders(requireContext(), feed.feedId, newFolders, oldFolders);
ChooseFoldersFragment.this.dismiss();
}
});
ListAdapter adapter = new ArrayAdapter<Folder>(getActivity(), R.layout.row_choosefolders, R.id.choosefolders_foldername, folders) {
ListAdapter adapter = new ArrayAdapter<>(requireContext(), R.layout.row_choosefolders, R.id.choosefolders_foldername, folders) {
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
View v = super.getView(position, convertView, parent);
CheckBox row = (CheckBox) v.findViewById(R.id.choosefolders_foldername);
CheckBox row = v.findViewById(R.id.choosefolders_foldername);
if (position == 0) {
row.setText(R.string.top_level);
}

View file

@ -82,11 +82,11 @@ public class DeleteFeedFragment extends DialogFragment {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
if (getArguments().getString(FEED_TYPE).equals(NORMAL_FEED)) {
feedUtils.deleteFeed(getArguments().getString(FEED_ID), getArguments().getString(FOLDER_NAME), getActivity());
feedUtils.deleteFeed(getArguments().getString(FEED_ID), getArguments().getString(FOLDER_NAME));
} else if (getArguments().getString(FEED_TYPE).equals(SAVED_SEARCH_FEED)) {
feedUtils.deleteSavedSearch(getArguments().getString(FEED_ID), getArguments().getString(QUERY), getActivity());
feedUtils.deleteSavedSearch(getArguments().getString(FEED_ID), getArguments().getString(QUERY));
} else {
feedUtils.deleteSocialFeed(getArguments().getString(FEED_ID), getActivity());
feedUtils.deleteSocialFeed(getArguments().getString(FEED_ID));
}
// if called from a feed view, end it
Activity activity = DeleteFeedFragment.this.getActivity();

View file

@ -1,13 +1,11 @@
package com.newsblur.fragment;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.EditText;
@ -43,33 +41,31 @@ public class EditReplyDialogFragment extends DialogFragment {
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Activity activity = getActivity();
final Story story = (Story) getArguments().getSerializable(STORY);
final String commentUserId = getArguments().getString(COMMENT_USER_ID);
final String replyId = getArguments().getString(REPLY_ID);
String replyText = getArguments().getString(REPLY_TEXT);
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
builder.setTitle(R.string.edit_reply);
LayoutInflater layoutInflater = LayoutInflater.from(activity);
View replyView = layoutInflater.inflate(R.layout.reply_dialog, null);
View replyView = getLayoutInflater().inflate(R.layout.reply_dialog, null);
builder.setView(replyView);
final EditText reply = (EditText) replyView.findViewById(R.id.reply_field);
final EditText reply = replyView.findViewById(R.id.reply_field);
reply.setText(replyText);
builder.setPositiveButton(R.string.edit_reply_update, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
String replyText = reply.getText().toString();
feedUtils.updateReply(activity, story, commentUserId, replyId, replyText);
feedUtils.updateReply(requireContext(), story, commentUserId, replyId, replyText);
EditReplyDialogFragment.this.dismiss();
}
});
builder.setNegativeButton(R.string.edit_reply_delete, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
feedUtils.deleteReply(activity, story, commentUserId, replyId);
feedUtils.deleteReply(requireContext(), story, commentUserId, replyId);
EditReplyDialogFragment.this.dismiss();
}
});

View file

@ -3,7 +3,6 @@ package com.newsblur.fragment;
import java.util.List;
import java.util.Map;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
@ -11,7 +10,6 @@ import android.os.Bundle;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
@ -58,15 +56,13 @@ public class FeedIntelTrainerFragment extends DialogFragment {
fs = (FeedSet) getArguments().getSerializable("feedset");
classifier = dbHelper.getClassifierForFeed(feed.feedId);
final Activity activity = getActivity();
LayoutInflater inflater = LayoutInflater.from(activity);
View v = inflater.inflate(R.layout.dialog_trainfeed, null);
View v = getLayoutInflater().inflate(R.layout.dialog_trainfeed, null);
binding = DialogTrainfeedBinding.bind(v);
// display known title classifiers
for (Map.Entry<String, Integer> rule : classifier.title.entrySet()) {
View row = inflater.inflate(R.layout.include_intel_row, null);
TextView label = (TextView) row.findViewById(R.id.intel_row_label);
View row = getLayoutInflater().inflate(R.layout.include_intel_row, null);
TextView label = row.findViewById(R.id.intel_row_label);
label.setText(rule.getKey());
UIUtils.setupIntelDialogRow(row, classifier.title, rule.getKey());
binding.existingTitleIntelContainer.addView(row);
@ -82,8 +78,8 @@ public class FeedIntelTrainerFragment extends DialogFragment {
}
}
for (String tag : allTags) {
View row = inflater.inflate(R.layout.include_intel_row, null);
TextView label = (TextView) row.findViewById(R.id.intel_row_label);
View row = getLayoutInflater().inflate(R.layout.include_intel_row, null);
TextView label = row.findViewById(R.id.intel_row_label);
label.setText(tag);
UIUtils.setupIntelDialogRow(row, classifier.tags, tag);
binding.existingTagIntelContainer.addView(row);
@ -99,8 +95,8 @@ public class FeedIntelTrainerFragment extends DialogFragment {
}
}
for (String author : allAuthors) {
View rowAuthor = inflater.inflate(R.layout.include_intel_row, null);
TextView labelAuthor = (TextView) rowAuthor.findViewById(R.id.intel_row_label);
View rowAuthor = getLayoutInflater().inflate(R.layout.include_intel_row, null);
TextView labelAuthor = rowAuthor.findViewById(R.id.intel_row_label);
labelAuthor.setText(author);
UIUtils.setupIntelDialogRow(rowAuthor, classifier.authors, author);
binding.existingAuthorIntelContainer.addView(rowAuthor);
@ -108,13 +104,13 @@ public class FeedIntelTrainerFragment extends DialogFragment {
if (allAuthors.size() < 1) binding.intelAuthorHeader.setVisibility(View.GONE);
// for feel-level intel, the label is the title and the intel identifier is the feed ID
View rowFeed = inflater.inflate(R.layout.include_intel_row, null);
TextView labelFeed = (TextView) rowFeed.findViewById(R.id.intel_row_label);
View rowFeed = getLayoutInflater().inflate(R.layout.include_intel_row, null);
TextView labelFeed = rowFeed.findViewById(R.id.intel_row_label);
labelFeed.setText(feed.title);
UIUtils.setupIntelDialogRow(rowFeed, classifier.feeds, feed.feedId);
binding.existingFeedIntelContainer.addView(rowFeed);
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
builder.setTitle(R.string.feed_intel_dialog_title);
builder.setView(v);
@ -127,7 +123,7 @@ public class FeedIntelTrainerFragment extends DialogFragment {
builder.setPositiveButton(R.string.dialog_story_intel_save, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
feedUtils.updateClassifier(feed.feedId, classifier, fs, activity);
feedUtils.updateClassifier(feed.feedId, classifier, fs, requireContext());
FeedIntelTrainerFragment.this.dismiss();
}
});

View file

@ -18,7 +18,7 @@ public class FeedSelectorFragment extends Fragment implements StateChangedListen
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View v = inflater.inflate(R.layout.fragment_intelligenceselector, null);
button = (StateToggleButton) v.findViewById(R.id.fragment_intelligence_statebutton);
button = v.findViewById(R.id.fragment_intelligence_statebutton);
button.setStateListener(this);
return v;
}

View file

@ -181,7 +181,7 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
binding.folderfeedList.setOnGroupCollapseListener(this);
binding.folderfeedList.setOnGroupExpandListener(this);
adapter.listBackref = new WeakReference(binding.folderfeedList); // see note in adapter about backref
adapter.listBackref = new WeakReference<>(binding.folderfeedList); // see note in adapter about backref
binding.folderfeedList.setAdapter(adapter);
// Main activity needs to listen for scrolls to prevent refresh from firing unnecessarily

View file

@ -19,6 +19,7 @@ import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import com.google.android.material.progressindicator.LinearProgressIndicator;
import com.newsblur.R;
import com.newsblur.activity.ItemsList;
import com.newsblur.activity.NbActivity;
@ -41,7 +42,6 @@ import com.newsblur.util.StoryListStyle;
import com.newsblur.util.ThumbnailStyle;
import com.newsblur.util.UIUtils;
import com.newsblur.util.ViewUtils;
import com.newsblur.view.ProgressThrobber;
import com.newsblur.viewModel.StoriesViewModel;
import javax.inject.Inject;
@ -67,7 +67,15 @@ public class ItemSetFragment extends NbFragment {
private static final String BUNDLE_GRIDSTATE = "gridstate";
protected boolean cursorSeenYet = false; // have we yet seen a valid cursor for our particular feedset?
private boolean cursorSeenYet = false; // have we yet seen a valid cursor for our particular feedset?
/**
* Flag used to ensure that when the adapter resumes,
* it omits any new stories that would disrupt the current order and cause the list to
* unexpectedly jump, thereby preserving the scroll position. This flag specifically helps
* manage the insertion of new stories that have been backfilled according to their timestamps.
*/
private boolean skipBackFillingStories = false;
private int itemGridWidthPx = 0;
private int columnCount;
@ -84,7 +92,7 @@ public class ItemSetFragment extends NbFragment {
// R.id.top_loading_throb
// loading indicator for when stories are present and fresh (at bottom of list)
protected ProgressThrobber bottomProgressView;
protected LinearProgressIndicator bottomProgressView;
// the fleuron has padding that can't be calculated until after layout, but only changes
// rarely thereafter
@ -119,6 +127,7 @@ public class ItemSetFragment extends NbFragment {
// readings and cause zero-index refreshes, wasting massive cycles. hold the refresh logic
// until the loaders reset
cursorSeenYet = false;
skipBackFillingStories = true;
super.onPause();
}
@ -149,14 +158,11 @@ public class ItemSetFragment extends NbFragment {
// disable the throbbers if animations are going to have a zero time scale
boolean isDisableAnimations = ViewUtils.isPowerSaveMode(requireContext());
int[] colorsArray = UIUtils.getLoadingColorsArray(requireContext());
binding.topLoadingThrob.setEnabled(!isDisableAnimations);
binding.topLoadingThrob.setColors(colorsArray);
binding.topLoadingIndicator.setEnabled(!isDisableAnimations);
View footerView = inflater.inflate(R.layout.row_loading_throbber, null);
bottomProgressView = footerView.findViewById(R.id.itemlist_loading_throb);
View footerView = inflater.inflate(R.layout.row_loading_indicator, null);
bottomProgressView = footerView.findViewById(R.id.itemlist_loading);
bottomProgressView.setEnabled(!isDisableAnimations);
bottomProgressView.setColors(colorsArray);
fleuronBinding.getRoot().setVisibility(View.INVISIBLE);
fleuronBinding.containerSubscribe.setOnClickListener(view -> UIUtils.startSubscriptionActivity(requireContext()));
@ -280,7 +286,7 @@ public class ItemSetFragment extends NbFragment {
}
protected void updateAdapter(@Nullable Cursor cursor) {
adapter.swapCursor(cursor, binding.itemgridfragmentGrid, gridState);
adapter.swapCursor(cursor, binding.itemgridfragmentGrid, gridState, skipBackFillingStories);
gridState = null;
adapter.updateFeedSet(getFeedSet());
if ((cursor != null) && (cursor.getCount() > 0)) {
@ -318,7 +324,7 @@ public class ItemSetFragment extends NbFragment {
if (cursorSeenYet && adapter.getRawStoryCount() > 0 && UIUtils.needsSubscriptionAccess(requireContext(), getFeedSet())) {
fleuronBinding.getRoot().setVisibility(View.VISIBLE);
fleuronBinding.containerSubscribe.setVisibility(View.VISIBLE);
binding.topLoadingThrob.setVisibility(View.INVISIBLE);
binding.topLoadingIndicator.setVisibility(View.INVISIBLE);
bottomProgressView.setVisibility(View.INVISIBLE);
fleuronResized = false;
return;
@ -330,10 +336,10 @@ public class ItemSetFragment extends NbFragment {
binding.emptyViewImage.setVisibility(View.INVISIBLE);
if (NBSyncService.isFeedSetStoriesFresh(getFeedSet())) {
binding.topLoadingThrob.setVisibility(View.INVISIBLE);
binding.topLoadingIndicator.setVisibility(View.INVISIBLE);
bottomProgressView.setVisibility(View.VISIBLE);
} else {
binding.topLoadingThrob.setVisibility(View.VISIBLE);
binding.topLoadingIndicator.setVisibility(View.VISIBLE);
bottomProgressView.setVisibility(View.GONE);
}
fleuronBinding.getRoot().setVisibility(View.INVISIBLE);
@ -347,7 +353,7 @@ public class ItemSetFragment extends NbFragment {
binding.emptyViewText.setTypeface(binding.emptyViewText.getTypeface(), Typeface.NORMAL);
binding.emptyViewImage.setVisibility(View.VISIBLE);
binding.topLoadingThrob.setVisibility(View.INVISIBLE);
binding.topLoadingIndicator.setVisibility(View.INVISIBLE);
bottomProgressView.setVisibility(View.INVISIBLE);
if (cursorSeenYet && NBSyncService.isFeedSetExhausted(getFeedSet()) && (adapter.getRawStoryCount() > 0)) {
fleuronBinding.containerSubscribe.setVisibility(View.GONE);

View file

@ -13,7 +13,6 @@ import com.newsblur.database.BlurDatabaseHelper
import com.newsblur.databinding.LoginasDialogBinding
import com.newsblur.network.APIManager
import com.newsblur.util.PrefsUtils
import com.newsblur.util.UIUtils
import com.newsblur.util.executeAsyncTask
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@ -50,7 +49,7 @@ class LoginAsDialogFragment : DialogFragment() {
val startMain = Intent(requireContext(), Main::class.java)
requireContext().startActivity(startMain)
} else {
UIUtils.safeToast(requireActivity(), "Login as $username failed", Toast.LENGTH_LONG)
Toast.makeText(requireActivity(), "Login as $username failed", Toast.LENGTH_LONG).show()
}
}
)

View file

@ -8,14 +8,13 @@ import android.view.View
import android.view.ViewGroup
import android.widget.*
import android.widget.AdapterView.OnItemClickListener
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.newsblur.R
import com.newsblur.activity.Profile
import com.newsblur.database.BlurDatabaseHelper
import com.newsblur.databinding.FragmentProfileactivityBinding
import com.newsblur.databinding.RowLoadingThrobberBinding
import com.newsblur.databinding.RowLoadingIndicatorBinding
import com.newsblur.di.IconLoader
import com.newsblur.domain.ActivityDetails
import com.newsblur.domain.UserDetails
@ -39,7 +38,7 @@ abstract class ProfileActivityDetailsFragment : Fragment(), OnItemClickListener
lateinit var iconLoader: ImageLoader
private lateinit var binding: FragmentProfileactivityBinding
private lateinit var footerBinding: RowLoadingThrobberBinding
private lateinit var footerBinding: RowLoadingIndicatorBinding
private var adapter: ActivityDetailsAdapter? = null
private var user: UserDetails? = null
@ -47,15 +46,12 @@ abstract class ProfileActivityDetailsFragment : Fragment(), OnItemClickListener
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_profileactivity, null)
binding = FragmentProfileactivityBinding.bind(view)
val colorsArray = UIUtils.getLoadingColorsArray(requireContext())
binding.emptyViewLoadingThrob.setColors(*colorsArray)
binding.profileDetailsActivitylist.setFooterDividersEnabled(false)
binding.profileDetailsActivitylist.emptyView = binding.emptyView
val footerView = inflater.inflate(R.layout.row_loading_throbber, null)
footerBinding = RowLoadingThrobberBinding.bind(footerView)
footerBinding.itemlistLoadingThrob.setColors(*colorsArray)
val footerView = inflater.inflate(R.layout.row_loading_indicator, null)
footerBinding = RowLoadingIndicatorBinding.bind(footerView)
binding.profileDetailsActivitylist.addFooterView(footerView, null, false)
if (adapter != null) {
displayActivities()
@ -80,8 +76,8 @@ abstract class ProfileActivityDetailsFragment : Fragment(), OnItemClickListener
private fun loadPage(pageNumber: Int) {
lifecycleScope.executeAsyncTask(
onPreExecute = {
binding.emptyViewLoadingThrob.visibility = View.VISIBLE
footerBinding.itemlistLoadingThrob.visibility = View.VISIBLE
binding.emptyViewLoading.visibility = View.VISIBLE
footerBinding.itemlistLoading.visibility = View.VISIBLE
},
doInBackground = {
// For the logged in user user.userId is null.
@ -106,8 +102,8 @@ abstract class ProfileActivityDetailsFragment : Fragment(), OnItemClickListener
adapter!!.add(activity)
}
adapter!!.notifyDataSetChanged()
binding.emptyViewLoadingThrob.visibility = View.GONE
footerBinding.itemlistLoadingThrob.visibility = View.GONE
binding.emptyViewLoading.visibility = View.GONE
footerBinding.itemlistLoading.visibility = View.GONE
}
)
}

View file

@ -33,10 +33,10 @@ import com.newsblur.domain.Story
import com.newsblur.domain.UserDetails
import com.newsblur.keyboard.KeyboardManager
import com.newsblur.network.APIManager
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_INTEL
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_SOCIAL
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_STORY
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_TEXT
import com.newsblur.service.NbSyncManager.UPDATE_INTEL
import com.newsblur.service.NbSyncManager.UPDATE_SOCIAL
import com.newsblur.service.NbSyncManager.UPDATE_STORY
import com.newsblur.service.NbSyncManager.UPDATE_TEXT
import com.newsblur.service.OriginalTextService
import com.newsblur.util.*
import com.newsblur.util.PrefConstants.ThemeValue
@ -422,7 +422,9 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener {
true
}
R.id.menu_go_to_feed -> {
FeedItemsList.startActivity(context, fs, dbHelper.getFeed(story!!.feedId), null, null)
val feed = dbHelper.getFeed(story!!.feedId)
val fs = FeedSet.singleFeed(feed.feedId)
FeedItemsList.startActivity(requireContext(), fs, feed, null, null)
true
}
else -> {

View file

@ -1,6 +1,5 @@
package com.newsblur.fragment;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
@ -8,7 +7,6 @@ import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Toast;
@ -58,12 +56,10 @@ public class RenameDialogFragment extends DialogFragment {
public Dialog onCreateDialog(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Activity activity = getActivity();
LayoutInflater inflater = LayoutInflater.from(activity);
View v = inflater.inflate(R.layout.dialog_rename, null);
View v = getLayoutInflater().inflate(R.layout.dialog_rename, null);
final DialogRenameBinding binding = DialogRenameBinding.bind(v);
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
builder.setView(v);
builder.setNegativeButton(R.string.alert_dialog_cancel, new DialogInterface.OnClickListener() {
@Override
@ -78,7 +74,7 @@ public class RenameDialogFragment extends DialogFragment {
builder.setPositiveButton(R.string.feed_name_save, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
feedUtils.renameFeed(activity, feed.feedId, binding.inputName.getText().toString());
feedUtils.renameFeed(requireContext(), feed.feedId, binding.inputName.getText().toString());
RenameDialogFragment.this.dismiss();
}
});
@ -94,7 +90,7 @@ public class RenameDialogFragment extends DialogFragment {
public void onClick(DialogInterface dialogInterface, int i) {
String newFolderName = binding.inputName.getText().toString();
if (TextUtils.isEmpty(newFolderName)) {
Toast.makeText(activity, R.string.add_folder_name, Toast.LENGTH_SHORT).show();
Toast.makeText(requireContext(), R.string.add_folder_name, Toast.LENGTH_SHORT).show();
return;
}
@ -102,7 +98,7 @@ public class RenameDialogFragment extends DialogFragment {
if (!TextUtils.isEmpty(folderParentName) && !folderParentName.equals(AppConstants.ROOT_FOLDER)) {
inFolder = folderParentName;
}
feedUtils.renameFolder(folderName, newFolderName, inFolder, activity);
feedUtils.renameFolder(folderName, newFolderName, inFolder, requireContext());
RenameDialogFragment.this.dismiss();
}
});

View file

@ -1,13 +1,11 @@
package com.newsblur.fragment;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.EditText;
@ -47,21 +45,18 @@ public class ReplyDialogFragment extends DialogFragment {
story = (Story) getArguments().getSerializable(STORY);
commentUserId = getArguments().getString(COMMENT_USER_ID);
final Activity activity = getActivity();
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
String shareString = getResources().getString(R.string.reply_to);
builder.setTitle(String.format(shareString, getArguments().getString(COMMENT_USERNAME)));
LayoutInflater layoutInflater = LayoutInflater.from(activity);
View replyView = layoutInflater.inflate(R.layout.reply_dialog, null);
View replyView = getLayoutInflater().inflate(R.layout.reply_dialog, null);
builder.setView(replyView);
final EditText reply = (EditText) replyView.findViewById(R.id.reply_field);
final EditText reply = replyView.findViewById(R.id.reply_field);
builder.setPositiveButton(R.string.alert_dialog_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
feedUtils.replyToComment(story.id, story.feedId, commentUserId, reply.getText().toString(), activity);
feedUtils.replyToComment(story.id, story.feedId, commentUserId, reply.getText().toString(), requireContext());
}
});
builder.setNegativeButton(R.string.alert_dialog_cancel, new DialogInterface.OnClickListener() {

View file

@ -1,6 +1,5 @@
package com.newsblur.fragment;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
@ -8,7 +7,6 @@ import android.os.Bundle;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.EditText;
@ -53,9 +51,8 @@ public class ShareDialogFragment extends DialogFragment {
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Activity activity = getActivity();
story = (Story) getArguments().getSerializable(STORY);
user = PrefsUtils.getUserDetails(activity);
user = PrefsUtils.getUserDetails(requireContext());
sourceUserId = getArguments().getString(SOURCE_USER_ID);
boolean hasBeenShared = false;
@ -70,13 +67,12 @@ public class ShareDialogFragment extends DialogFragment {
previousComment = dbHelper.getComment(story.id, user.id);
}
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
builder.setTitle(String.format(getResources().getString(R.string.share_save_newsblur), UIUtils.fromHtml(story.title)));
LayoutInflater layoutInflater = LayoutInflater.from(activity);
View replyView = layoutInflater.inflate(R.layout.share_dialog, null);
View replyView = getLayoutInflater().inflate(R.layout.share_dialog, null);
builder.setView(replyView);
commentEditText = (EditText) replyView.findViewById(R.id.comment_field);
commentEditText = replyView.findViewById(R.id.comment_field);
int positiveButtonText = R.string.share_this_story;
int negativeButtonText = R.string.alert_dialog_cancel;
@ -92,7 +88,7 @@ public class ShareDialogFragment extends DialogFragment {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
String shareComment = commentEditText.getText().toString();
feedUtils.shareStory(story, shareComment, sourceUserId, activity);
feedUtils.shareStory(story, shareComment, sourceUserId, requireContext());
ShareDialogFragment.this.dismiss();
}
});
@ -101,7 +97,7 @@ public class ShareDialogFragment extends DialogFragment {
builder.setNegativeButton(negativeButtonText, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
feedUtils.unshareStory(story, activity);
feedUtils.unshareStory(story, requireContext());
ShareDialogFragment.this.dismiss();
}
});

View file

@ -110,7 +110,7 @@ public class StoryIntelTrainerFragment extends DialogFragment {
for (Map.Entry<String, Integer> rule : classifier.title.entrySet()) {
if (story.title.indexOf(rule.getKey()) >= 0) {
View row = getLayoutInflater().inflate(R.layout.include_intel_row, null);
TextView label = (TextView) row.findViewById(R.id.intel_row_label);
TextView label = row.findViewById(R.id.intel_row_label);
label.setText(rule.getKey());
UIUtils.setupIntelDialogRow(row, classifier.title, rule.getKey());
binding.existingTitleIntelContainer.addView(row);
@ -120,7 +120,7 @@ public class StoryIntelTrainerFragment extends DialogFragment {
// list all tags for this story, trained or not
for (String tag : story.tags) {
View row = getLayoutInflater().inflate(R.layout.include_intel_row, null);
TextView label = (TextView) row.findViewById(R.id.intel_row_label);
TextView label = row.findViewById(R.id.intel_row_label);
label.setText(tag);
UIUtils.setupIntelDialogRow(row, classifier.tags, tag);
binding.existingTagIntelContainer.addView(row);
@ -130,7 +130,7 @@ public class StoryIntelTrainerFragment extends DialogFragment {
// there is a single author per story
if (!TextUtils.isEmpty(story.authors)) {
View rowAuthor = getLayoutInflater().inflate(R.layout.include_intel_row, null);
TextView labelAuthor = (TextView) rowAuthor.findViewById(R.id.intel_row_label);
TextView labelAuthor = rowAuthor.findViewById(R.id.intel_row_label);
labelAuthor.setText(story.authors);
UIUtils.setupIntelDialogRow(rowAuthor, classifier.authors, story.authors);
binding.existingAuthorIntelContainer.addView(rowAuthor);
@ -141,7 +141,7 @@ public class StoryIntelTrainerFragment extends DialogFragment {
// there is a single feed to be trained, but it is a bit odd in that the label is the title and
// the intel identifier is the feed ID
View rowFeed = getLayoutInflater().inflate(R.layout.include_intel_row, null);
TextView labelFeed = (TextView) rowFeed.findViewById(R.id.intel_row_label);
TextView labelFeed = rowFeed.findViewById(R.id.intel_row_label);
labelFeed.setText(feedUtils.getFeedTitle(story.feedId));
UIUtils.setupIntelDialogRow(rowFeed, classifier.feeds, story.feedId);
binding.existingFeedIntelContainer.addView(rowFeed);

View file

@ -59,7 +59,7 @@ class StoryUserTagsFragment : DialogFragment(), TagsAdapter.OnTagClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
storyUserTagsViewModel = ViewModelProvider(this).get(StoryUserTagsViewModel::class.java)
storyUserTagsViewModel = ViewModelProvider(this)[StoryUserTagsViewModel::class.java]
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {

View file

@ -88,7 +88,6 @@ public class APIResponse {
} catch (IOException ioe) {
com.newsblur.util.Log.e(this.getClass().getName(), "Error (" + ioe.getMessage() + ") calling " + request.url().toString(), ioe);
this.isError = true;
return;
}
}
@ -116,7 +115,7 @@ public class APIResponse {
try {
T response = classOfT.newInstance();
response.isProtocolError = true;
return ((T) response);
return response;
} catch (Exception e) {
// this should never fail unless the constructor of the base response bean fails
Log.wtf(this.getClass().getName(), "Failed to load class: " + classOfT);

View file

@ -22,16 +22,16 @@ public class ClassifierMapTypeAdapter implements JsonDeserializer<Map<String,Cla
@Override
public Map<String,Classifier> deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
Map<String,Classifier> result = new HashMap<String,Classifier>();
Map<String,Classifier> result = new HashMap<>();
if (jsonElement.isJsonObject()) {
JsonObject o = jsonElement.getAsJsonObject();
if (o.get("authors") != null) { // this is our hint that this is a bare classifiers object
Classifier c = (Classifier) jsonDeserializationContext.deserialize(jsonElement, Classifier.class);
Classifier c = jsonDeserializationContext.deserialize(jsonElement, Classifier.class);
result.put( "-1", c);
} else { // otherwise, we have a map of IDs to classifiers
for (Map.Entry<String, JsonElement> entry : o.entrySet()) {
Classifier c = (Classifier) jsonDeserializationContext.deserialize(entry.getValue(), Classifier.class);
Classifier c = jsonDeserializationContext.deserialize(entry.getValue(), Classifier.class);
result.put(entry.getKey(), c);
}
}

View file

@ -7,8 +7,10 @@ import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.newsblur.NbApplication;
import com.newsblur.domain.Story;
import com.newsblur.network.APIConstants;
import com.newsblur.util.StoryUtil;
import com.newsblur.util.UIUtils;
import java.lang.reflect.Type;
@ -43,8 +45,15 @@ public class StoryTypeAdapter implements JsonDeserializer<Story> {
story.timestamp = story.timestamp * 1000;
story.starredTimestamp = story.starredTimestamp * 1000;
// due to android.os.TransactionTooLargeException and
// android.database.sqlite.SQLiteBlobTooBigException
// truncate the story's content in case it's large
if (!NbApplication.isAppForeground()) {
story.content = StoryUtil.truncateContent(story.content);
}
// replace http image urls with https
if (httpSniff.matcher(story.content).find() && story.secureImageUrls != null && story.secureImageUrls.size() > 0) {
if (httpSniff.matcher(story.content).find() && story.secureImageUrls != null && !story.secureImageUrls.isEmpty()) {
for (String url : story.secureImageUrls.keySet()) {
if (httpSniff.matcher(url).find()) {
String secureUrl = story.secureImageUrls.get(url);

View file

@ -1,31 +0,0 @@
package com.newsblur.service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
abstract class NBSyncReceiver : BroadcastReceiver() {
abstract fun handleUpdateType(updateType: Int)
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == NB_SYNC_ACTION) {
handleUpdateType(intent.getIntExtra(NB_SYNC_UPDATE_TYPE, 0))
}
}
companion object {
const val NB_SYNC_ACTION = "nb.sync.action"
const val NB_SYNC_ERROR_MESSAGE = "nb_sync_error_msg"
const val NB_SYNC_UPDATE_TYPE = "nb_sync_update_type"
const val UPDATE_DB_READY = 1 shl 0
const val UPDATE_METADATA = 1 shl 1
const val UPDATE_STORY = 1 shl 2
const val UPDATE_SOCIAL = 1 shl 3
const val UPDATE_INTEL = 1 shl 4
const val UPDATE_STATUS = 1 shl 5
const val UPDATE_TEXT = 1 shl 6
const val UPDATE_REBUILD = 1 shl 7
}
}

View file

@ -13,11 +13,11 @@ import com.newsblur.NbApplication;
import com.newsblur.R;
import com.newsblur.database.BlurDatabaseHelper;
import static com.newsblur.database.BlurDatabaseHelper.closeQuietly;
import static com.newsblur.service.NBSyncReceiver.UPDATE_DB_READY;
import static com.newsblur.service.NBSyncReceiver.UPDATE_METADATA;
import static com.newsblur.service.NBSyncReceiver.UPDATE_REBUILD;
import static com.newsblur.service.NBSyncReceiver.UPDATE_STATUS;
import static com.newsblur.service.NBSyncReceiver.UPDATE_STORY;
import static com.newsblur.service.NbSyncManager.UPDATE_DB_READY;
import static com.newsblur.service.NbSyncManager.UPDATE_METADATA;
import static com.newsblur.service.NbSyncManager.UPDATE_REBUILD;
import static com.newsblur.service.NbSyncManager.UPDATE_STATUS;
import static com.newsblur.service.NbSyncManager.UPDATE_STORY;
import androidx.annotation.NonNull;
@ -47,10 +47,7 @@ import com.newsblur.util.NetworkUtils;
import com.newsblur.util.NotificationUtils;
import com.newsblur.util.PrefsUtils;
import com.newsblur.util.ReadingAction;
import com.newsblur.util.ReadFilter;
import com.newsblur.util.StateFilter;
import com.newsblur.util.StoryOrder;
import com.newsblur.util.UIUtils;
import com.newsblur.widget.WidgetUtils;
import java.util.ArrayList;
@ -272,6 +269,12 @@ public class NBSyncService extends JobService {
return false;
}
@Override
public void onNetworkChanged(@NonNull JobParameters params) {
super.onNetworkChanged(params);
com.newsblur.util.Log.d(this, "onNetworkChanged");
}
/**
* Do the actual work of syncing.
*/
@ -1160,7 +1163,7 @@ public class NBSyncService extends JobService {
dbHelper.prepareReadingSession(fs, cursorFilters.getStateFilter(), cursorFilters.getReadFilter());
// note which feedset we are loading so we can trigger another reset when it changes
dbHelper.setSessionFeedSet(fs);
UIUtils.syncUpdateStatus(context, UPDATE_STORY | UPDATE_STATUS);
NbSyncManager.submitUpdate(UPDATE_STORY | UPDATE_STATUS);
}
}
}
@ -1270,20 +1273,10 @@ public class NBSyncService extends JobService {
}
protected void sendSyncUpdate(int update) {
Intent i = new Intent(NBSyncReceiver.NB_SYNC_ACTION);
i.putExtra(NBSyncReceiver.NB_SYNC_UPDATE_TYPE, update);
broadcastSync(i);
NbSyncManager.submitUpdate(update);
}
protected void sendToastError(@NonNull String message) {
Intent i = new Intent(NBSyncReceiver.NB_SYNC_ACTION);
i.putExtra(NBSyncReceiver.NB_SYNC_ERROR_MESSAGE, message);
broadcastSync(i);
}
private void broadcastSync(@NonNull Intent intent) {
if (NbApplication.isAppForeground()) {
sendBroadcast(intent);
}
NbSyncManager.submitError(message);
}
}

View file

@ -0,0 +1,45 @@
package com.newsblur.service
import com.newsblur.util.NBScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
object NbSyncManager {
const val UPDATE_DB_READY = 1 shl 0
const val UPDATE_METADATA = 1 shl 1
const val UPDATE_STORY = 1 shl 2
const val UPDATE_SOCIAL = 1 shl 3
const val UPDATE_INTEL = 1 shl 4
const val UPDATE_STATUS = 1 shl 5
const val UPDATE_TEXT = 1 shl 6
const val UPDATE_REBUILD = 1 shl 7
private val _state = MutableSharedFlow<NBSync>()
val state = _state.asSharedFlow()
@JvmStatic
fun submitUpdate(type: Int) = submit(NBSync.Update(type))
@JvmStatic
fun submitError(msg: String) = submit(NBSync.Error(msg))
private fun submit(nbSync: NBSync) {
NBScope.launch(Dispatchers.IO) {
_state.emit(nbSync)
}
}
}
sealed class NBSync {
data class Error(
val msg: String,
) : NBSync()
data class Update(
val type: Int,
) : NBSync()
}

View file

@ -1,6 +1,6 @@
package com.newsblur.service;
import static com.newsblur.service.NBSyncReceiver.UPDATE_TEXT;
import static com.newsblur.service.NbSyncManager.UPDATE_TEXT;
import com.newsblur.database.DatabaseConstants;
import com.newsblur.network.domain.StoryTextResponse;

View file

@ -36,7 +36,7 @@ abstract class SubService(
if (isActive) {
parent.checkCompletion()
parent.sendSyncUpdate(NBSyncReceiver.UPDATE_STATUS)
parent.sendSyncUpdate(NbSyncManager.UPDATE_STATUS)
}
}
}

View file

@ -139,8 +139,8 @@ public class UnreadsService extends SubService {
boolean isTextPrefetchEnabled = PrefsUtils.isTextPrefetchEnabled(parent);
if (! (isOfflineEnabled || isEnableNotifications)) return;
List<String> hashBatch = new ArrayList(AppConstants.UNREAD_FETCH_BATCH_SIZE);
List<String> hashSkips = new ArrayList(AppConstants.UNREAD_FETCH_BATCH_SIZE);
List<String> hashBatch = new ArrayList<>(AppConstants.UNREAD_FETCH_BATCH_SIZE);
List<String> hashSkips = new ArrayList<>(AppConstants.UNREAD_FETCH_BATCH_SIZE);
batchloop: for (String hash : StoryHashQueue) {
if( isOfflineEnabled ||
(isEnableNotifications && notifyFeeds.contains(FeedUtils.inferFeedId(hash))) ) {

View file

@ -0,0 +1,56 @@
package com.newsblur.util
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Environment
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.core.net.toUri
import com.newsblur.R
import com.newsblur.network.APIConstants
object FileDownloader {
fun exportOpml(context: Context): Long {
val manager = context.getSystemService(DownloadManager::class.java)
val url = APIConstants.buildUrl(APIConstants.PATH_EXPORT_OPML)
val userName = PrefsUtils.getUserName(context)
val cookie = PrefsUtils.getCookie(context)
val file = StringBuilder().apply {
append(context.getString(R.string.newsbluropml))
userName?.let { append("-$userName") }
append(".xml")
}.toString()
val request = DownloadManager.Request(url.toUri())
.setMimeType(MimeTypeMap.getSingleton().getMimeTypeFromExtension(".xml"))
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.addRequestHeader("Cookie", cookie)
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, file)
.setTitle(context.getString(R.string.newsblur_opml))
return manager.enqueue(request)
}
}
class DownloadCompleteReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == DownloadManager.ACTION_DOWNLOAD_COMPLETE) {
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1L)
if (id == expectedFileDownloadId) {
context?.let {
val msg = "${it.getString(R.string.newsblur_opml)} download completed"
Toast.makeText(it, msg, Toast.LENGTH_SHORT).show()
}
}
}
}
companion object {
var expectedFileDownloadId: Long? = null
}
}

View file

@ -23,6 +23,7 @@ object FeedExt {
fun Feed.disableNotification() {
notificationFilter = null
disableNotificationType(NOTIFY_ANDROID)
}
@JvmStatic
@ -39,10 +40,12 @@ object FeedExt {
fun Feed.setNotifyFocus() {
notificationFilter = Feed.NOTIFY_FILTER_FOCUS
enableNotificationType(NOTIFY_ANDROID)
}
fun Feed.setNotifyUnread() {
notificationFilter = Feed.NOTIFY_FILTER_UNREAD
enableNotificationType(NOTIFY_ANDROID)
}
private fun Feed.isNotify(type: String): Boolean = notificationTypes?.contains(type) ?: false

View file

@ -80,7 +80,7 @@ public class FeedSet implements Serializable {
*/
public static FeedSet allFeeds() {
FeedSet fs = new FeedSet();
fs.feeds = Collections.EMPTY_SET;
fs.feeds = Collections.emptySet();
return fs;
}
@ -107,7 +107,7 @@ public class FeedSet implements Serializable {
*/
public static FeedSet allSaved() {
FeedSet fs = new FeedSet();
fs.savedTags = Collections.EMPTY_SET;
fs.savedTags = Collections.emptySet();
return fs;
}
@ -116,7 +116,7 @@ public class FeedSet implements Serializable {
*/
public static FeedSet singleSavedTag(String tag) {
FeedSet fs = new FeedSet();
fs.savedTags = new HashSet<String>(1);
fs.savedTags = new HashSet<>(1);
fs.savedTags.add(tag);
fs.savedTags = Collections.unmodifiableSet(fs.savedTags);
return fs;
@ -146,7 +146,7 @@ public class FeedSet implements Serializable {
*/
public static FeedSet allSocialFeeds() {
FeedSet fs = new FeedSet();
fs.socialFeeds = Collections.EMPTY_MAP;
fs.socialFeeds = Collections.emptyMap();
return fs;
}
@ -161,7 +161,7 @@ public class FeedSet implements Serializable {
fs.feeds.addAll(feedIds);
fs.feeds = Collections.unmodifiableSet(fs.feeds);
} else {
fs.feeds = Collections.EMPTY_SET;
fs.feeds = Collections.emptySet();
}
return fs;
}

View file

@ -4,6 +4,7 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.text.TextUtils
import com.newsblur.NbApplication
import com.newsblur.R
import com.newsblur.activity.NbActivity
import com.newsblur.database.BlurDatabaseHelper
@ -13,14 +14,15 @@ import com.newsblur.domain.Story
import com.newsblur.fragment.ReadingActionConfirmationFragment
import com.newsblur.network.APIConstants
import com.newsblur.network.APIManager
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_METADATA
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_SOCIAL
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_STORY
import com.newsblur.service.NBSyncService
import com.newsblur.service.NbSyncManager
import com.newsblur.service.NbSyncManager.UPDATE_METADATA
import com.newsblur.service.NbSyncManager.UPDATE_SOCIAL
import com.newsblur.service.NbSyncManager.UPDATE_STORY
import com.newsblur.service.OriginalTextService
import com.newsblur.util.FeedExt.disableNotification
import com.newsblur.util.FeedExt.setNotifyFocus
import com.newsblur.util.FeedExt.setNotifyUnread
import com.newsblur.util.UIUtils.syncUpdateStatus
class FeedUtils(
private val dbHelper: BlurDatabaseHelper,
@ -66,14 +68,14 @@ class FeedUtils(
doInBackground = {
val ra = if (saved) ReadingAction.saveStory(storyHash, userTags) else ReadingAction.unsaveStory(storyHash)
ra.doLocal(context, dbHelper)
syncUpdateStatus(context, UPDATE_STORY)
syncUpdateStatus(UPDATE_STORY)
dbHelper.enqueueAction(ra)
triggerSync(context)
}
)
}
fun deleteSavedSearch(feedId: String?, query: String?, context: Context) {
fun deleteSavedSearch(feedId: String?, query: String?) {
NBScope.executeAsyncTask(
doInBackground = {
apiManager.deleteSearch(feedId, query)
@ -81,7 +83,7 @@ class FeedUtils(
onPostExecute = { newsBlurResponse ->
if (!newsBlurResponse.isError) {
dbHelper.deleteSavedSearch(feedId, query)
syncUpdateStatus(context, UPDATE_METADATA)
syncUpdateStatus(UPDATE_METADATA)
}
}
)
@ -101,7 +103,7 @@ class FeedUtils(
)
}
fun deleteFeed(feedId: String?, folderName: String?, context: Context) {
fun deleteFeed(feedId: String?, folderName: String?) {
NBScope.executeAsyncTask(
doInBackground = {
apiManager.deleteFeed(feedId, folderName)
@ -109,12 +111,12 @@ class FeedUtils(
onPostExecute = {
// TODO: we can't check result.isError() because the delete call sets the .message property on all calls. find a better error check
dbHelper.deleteFeed(feedId)
syncUpdateStatus(context, UPDATE_METADATA)
syncUpdateStatus(UPDATE_METADATA)
}
)
}
fun deleteSocialFeed(userId: String?, context: Context) {
fun deleteSocialFeed(userId: String?) {
NBScope.executeAsyncTask(
doInBackground = {
apiManager.unfollowUser(userId)
@ -122,7 +124,7 @@ class FeedUtils(
onPostExecute = {
// TODO: we can't check result.isError() because the delete call sets the .message property on all calls. find a better error check
dbHelper.deleteSocialFeed(userId)
syncUpdateStatus(context, UPDATE_METADATA)
syncUpdateStatus(UPDATE_METADATA)
}
)
}
@ -189,7 +191,7 @@ class FeedUtils(
// update unread state and unread counts in the local DB
val impactedFeeds = dbHelper.setStoryReadState(story, read)
syncUpdateStatus(context, UPDATE_STORY)
syncUpdateStatus(UPDATE_STORY)
NBSyncService.addRecountCandidates(impactedFeeds)
triggerSync(context)
@ -309,7 +311,7 @@ class FeedUtils(
doInBackground = {
dbHelper.enqueueAction(ra)
val impact = ra.doLocal(context, dbHelper)
syncUpdateStatus(context, impact)
syncUpdateStatus(impact)
triggerSync(context)
}
)
@ -333,7 +335,9 @@ class FeedUtils(
fun sendStoryFull(story: Story?, context: Context) {
if (story == null) return
var body = getStoryText(story.storyHash)
if (TextUtils.isEmpty(body)) body = getStoryContent(story.storyHash)
if (body.isNullOrEmpty() || body == OriginalTextService.NULL_STORY_TEXT) {
body = getStoryContent(story.storyHash)
}
val intent = Intent(Intent.ACTION_SEND)
intent.type = "text/plain"
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
@ -350,7 +354,7 @@ class FeedUtils(
val ra = ReadingAction.shareStory(story.storyHash, story.id, story.feedId, sourceUserId, comment)
dbHelper.enqueueAction(ra)
ra.doLocal(context, dbHelper)
syncUpdateStatus(context, UPDATE_SOCIAL or UPDATE_STORY)
syncUpdateStatus(UPDATE_SOCIAL or UPDATE_STORY)
triggerSync(context)
}
@ -358,7 +362,7 @@ class FeedUtils(
val ra = ReadingAction.renameFeed(feedId, newFeedName)
dbHelper.enqueueAction(ra)
val impact = ra.doLocal(context, dbHelper)
syncUpdateStatus(context, impact)
syncUpdateStatus(impact)
triggerSync(context)
}
@ -366,7 +370,7 @@ class FeedUtils(
val ra = ReadingAction.unshareStory(story.storyHash, story.id, story.feedId)
dbHelper.enqueueAction(ra)
ra.doLocal(context, dbHelper)
syncUpdateStatus(context, UPDATE_SOCIAL or UPDATE_STORY)
syncUpdateStatus(UPDATE_SOCIAL or UPDATE_STORY)
triggerSync(context)
}
@ -374,7 +378,7 @@ class FeedUtils(
val ra = ReadingAction.likeComment(story.id, commentUserId, story.feedId)
dbHelper.enqueueAction(ra)
ra.doLocal(context, dbHelper)
syncUpdateStatus(context, UPDATE_SOCIAL)
syncUpdateStatus(UPDATE_SOCIAL)
triggerSync(context)
}
@ -382,7 +386,7 @@ class FeedUtils(
val ra = ReadingAction.unlikeComment(story.id, commentUserId, story.feedId)
dbHelper.enqueueAction(ra)
ra.doLocal(context, dbHelper)
syncUpdateStatus(context, UPDATE_SOCIAL)
syncUpdateStatus(UPDATE_SOCIAL)
triggerSync(context)
}
@ -390,7 +394,7 @@ class FeedUtils(
val ra = ReadingAction.replyToComment(storyId, feedId, commentUserId, replyText)
dbHelper.enqueueAction(ra)
ra.doLocal(context, dbHelper)
syncUpdateStatus(context, UPDATE_SOCIAL)
syncUpdateStatus(UPDATE_SOCIAL)
triggerSync(context)
}
@ -398,7 +402,7 @@ class FeedUtils(
val ra = ReadingAction.updateReply(story.id, story.feedId, commentUserId, replyId, replyText)
dbHelper.enqueueAction(ra)
ra.doLocal(context, dbHelper)
syncUpdateStatus(context, UPDATE_SOCIAL)
syncUpdateStatus(UPDATE_SOCIAL)
triggerSync(context)
}
@ -406,7 +410,7 @@ class FeedUtils(
val ra = ReadingAction.deleteReply(story.id, story.feedId, commentUserId, replyId)
dbHelper.enqueueAction(ra)
ra.doLocal(context, dbHelper)
syncUpdateStatus(context, UPDATE_SOCIAL)
syncUpdateStatus(UPDATE_SOCIAL)
triggerSync(context)
}
@ -452,7 +456,7 @@ class FeedUtils(
dbHelper.enqueueAction(ra)
ra.doLocal(context, dbHelper)
syncUpdateStatus(context, UPDATE_METADATA)
syncUpdateStatus(UPDATE_METADATA)
triggerSync(context)
}
)
@ -462,7 +466,7 @@ class FeedUtils(
val ra = ReadingAction.instaFetch(feedId)
dbHelper.enqueueAction(ra)
ra.doLocal(context, dbHelper)
syncUpdateStatus(context, UPDATE_METADATA)
syncUpdateStatus(UPDATE_METADATA)
triggerSync(context)
}
@ -484,6 +488,12 @@ class FeedUtils(
UIUtils.handleUri(context, Uri.parse(url))
}
private fun syncUpdateStatus(update: Int) {
if (NbApplication.isAppForeground) {
NbSyncManager.submitUpdate(update)
}
}
companion object {
@JvmStatic

View file

@ -25,7 +25,7 @@ object PendingIntentUtils {
requestCode: Int,
intent: Intent,
flags: Int,
): PendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
): PendingIntent? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.getBroadcast(context, requestCode, intent, flags or PendingIntent.FLAG_IMMUTABLE)
} else {
PendingIntent.getBroadcast(context, requestCode, intent, flags)

View file

@ -285,6 +285,12 @@ public class PrefsUtils {
return user;
}
@Nullable
public static String getUserName(Context context) {
SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
return preferences.getString(PrefConstants.USER_USERNAME, null);
}
private static void saveUserImage(final Context context, String pictureUrl) {
Bitmap bitmap;
try {
@ -1079,8 +1085,13 @@ public class PrefsUtils {
* which gets saved when a user is authenticated.
*/
public static boolean hasCookie(Context context) {
return getCookie(context) != null;
}
@Nullable
public static String getCookie(Context context) {
SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, Context.MODE_PRIVATE);
return preferences.getString(PrefConstants.PREF_COOKIE, null) != null;
return preferences.getString(PrefConstants.PREF_COOKIE, null);
}
public static MarkStoryReadBehavior getMarkStoryReadBehavior(Context context) {

View file

@ -1,9 +1,9 @@
package com.newsblur.util;
import static com.newsblur.service.NBSyncReceiver.UPDATE_INTEL;
import static com.newsblur.service.NBSyncReceiver.UPDATE_METADATA;
import static com.newsblur.service.NBSyncReceiver.UPDATE_SOCIAL;
import static com.newsblur.service.NBSyncReceiver.UPDATE_STORY;
import static com.newsblur.service.NbSyncManager.UPDATE_INTEL;
import static com.newsblur.service.NbSyncManager.UPDATE_METADATA;
import static com.newsblur.service.NbSyncManager.UPDATE_SOCIAL;
import static com.newsblur.service.NbSyncManager.UPDATE_STORY;
import android.content.ContentValues;
import android.content.Context;
@ -18,7 +18,7 @@ import com.newsblur.network.domain.CommentResponse;
import com.newsblur.network.domain.NewsBlurResponse;
import com.newsblur.network.domain.StoriesResponse;
import com.newsblur.network.APIManager;
import com.newsblur.service.NBSyncReceiver;
import com.newsblur.service.NbSyncManager;
import com.newsblur.service.NBSyncService;
import java.util.ArrayList;
@ -375,7 +375,7 @@ public class ReadingAction implements Serializable {
} else {
com.newsblur.util.Log.w(this, "failed to refresh story data after action");
}
impact |= NBSyncReceiver.UPDATE_SOCIAL;
impact |= NbSyncManager.UPDATE_SOCIAL;
}
if (commentResponse != null) {
result = commentResponse;
@ -384,7 +384,7 @@ public class ReadingAction implements Serializable {
} else {
com.newsblur.util.Log.w(this, "failed to refresh comment data after action");
}
impact |= NBSyncReceiver.UPDATE_SOCIAL;
impact |= NbSyncManager.UPDATE_SOCIAL;
}
if (result != null && impact != 0) {
result.impactCode = impact;

Some files were not shown because too many files have changed in this diff Show more