message:
When answering a metadata call, update the server state with the new metadata
We used to do that only in `server_0_p_10_and_later.ex`, I move it to
`server.ex`

diff:
@@ -586,7 +586,11 @@ defmodule KafkaEx.Server do
state.api_versions
)
- updated_state = %{state | correlation_id: correlation_id}
+ updated_state = %{
+ state
+ | metadata: metadata,
+ correlation_id: correlation_id
+ }
{:reply, metadata, updated_state}
end

diff:
@@ -163,29 +163,6 @@ defmodule KafkaEx.Server0P10AndLater do
{:ok, state}
end
- def kafka_server_metadata(topic, state) do
- {correlation_id, metadata} =
- retrieve_metadata(
- state.brokers,
- state.correlation_id,
- config_sync_timeout(),
- topic,
- state.api_versions
- )
-
- updated_state = %{
- state
- | metadata: metadata,
- correlation_id: correlation_id
- }
-
- {:reply, metadata, updated_state}
- end
-
- def kafka_server_update_metadata(state) do
- {:noreply, update_metadata(state)}
- end
-
def kafka_server_api_versions(state) do
response =
state.correlation_id


message:
Update workflows to work with OTP
Lock ubuntu version to 20.04
Steps names to make it more explicit
Fix OTP

change_type: MODIFY

diff:
@@ -7,52 +7,49 @@ on:
branches: [ master ]
jobs:
- setup:
- name: check / setup
- runs-on: ubuntu-latest
+ dependencies:
+ name: check | setup dependencies
+ runs-on: ubuntu-20.04
env:
MIX_ENV: dev
ACTIONS_ALLOW_UNSECURE_COMMANDS: true
-
strategy:
matrix:
- include:
- - elixir: 1.10.2
- erlang: 22.3
+ elixir: ['1.12.3']
+ erlang: ['24.3']
+
steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-elixir@v1
+ - name: Cancel previous runs
+ uses: styfle/cancel-workflow-action@0.9.0
+ with:
+ access_token: ${{ github.token }}
+ - name: Checkout Github repo
+ uses: actions/checkout@v2
+ - name: Setup elixir & erlang enviroment
+ uses: actions/setup-elixir@v1
with:
elixir-version: ${{matrix.elixir}} # Define the elixir version [required]
otp-version: ${{matrix.erlang}} # Define the OTP version [required]
-
- - name: Retrieve Mix Dependencies Cache
- uses: actions/cache@v2
- id: mix-cache # id to use in retrieve action
- with:
- path: deps
- key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
-
- - name: Retrieve Mix Dependencies Compilation Cache
+ experimental-otp: true # More info https://github.com/actions/setup-elixir/issues/31
+ - name: Retrieve cached dependencies
uses: actions/cache@v2
- id: mix-deps-compile-cache # id to use in retrieve action
+ id: mix-cache
with:
- path: _build
- key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix-deps-compile-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
- - name: Install Mix Dependencies
+ path: |
+ deps
+ _build
+ key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('mix.lock') }}
+ - name: Install dependencies
if: steps.mix-cache.outputs.cache-hit != 'true'
run: |
mix local.rebar --force
mix local.hex --force
mix deps.get
-
- - name: Compile Mix Dependencies
- if: steps.mix-deps-compile-cache.outputs.cache-hit != 'true'
- run: mix deps.compile
+ mix deps.compile
run-checks:
name: check
- runs-on: ubuntu-latest
+ runs-on: ubuntu-20.04
needs: [setup]
env:
MIX_ENV: dev
@@ -70,10 +67,12 @@ jobs:
steps:
- uses: actions/checkout@v2
- - uses: actions/setup-elixir@v1
+ - name: Setup elixir & erlang enviroment
+ uses: actions/setup-elixir@v1
with:
elixir-version: ${{matrix.elixir}} # Define the elixir version [required]
otp-version: ${{matrix.erlang}} # Define the OTP version [required]
+ experimental-otp: true # More info https://github.com/actions/setup-elixir/issues/31
- name: Retrieve Mix Dependencies Cache
uses: actions/cache@v2

diff:
@@ -8,22 +8,28 @@ on:
jobs:
setup:
- name: runner / setup
- runs-on: ubuntu-latest
+ name: test / setup
+ runs-on: ubuntu-20.04
env:
MIX_ENV: test
-
strategy:
matrix:
- include:
- - elixir: 1.10.2
- erlang: 22.3
+ elixir: ['1.12.3']
+ erlang: ['24.3']
+
steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-elixir@v1
+ - name: Cancel previous runs
+ uses: styfle/cancel-workflow-action@0.9.0
+ with:
+ access_token: ${{ github.token }}
+ - name: Checkout Github repo
+ uses: actions/checkout@v2
+ - name: Setup elixir & erlang enviroment
+ uses: actions/setup-elixir@v1
with:
elixir-version: ${{matrix.elixir}} # Define the elixir version [required]
otp-version: ${{matrix.erlang}} # Define the OTP version [required]
+ experimental-otp: true # More info https://github.com/actions/setup-elixir/issues/31
- name: Retrieve Mix Dependencies Cache
uses: actions/cache@v2
@@ -53,7 +59,7 @@ jobs:
name: runner / Test
needs: [setup]
- runs-on: ubuntu-latest
+ runs-on: ubuntu-20.04
env:
MIX_ENV: test
@@ -71,18 +77,17 @@ jobs:
- mix test
include:
- - elixir: 1.10.2
- erlang: 22.3
+ - elixir: 1.12.3
+ erlang: 24.3
steps:
- uses: actions/checkout@v2
- - name: Docker-compose up
- run: ./scripts/docker_up.sh
- - name: Setup elixir
+ - name: Setup elixir & erlang enviroment
uses: actions/setup-elixir@v1
with:
elixir-version: ${{matrix.elixir}} # Define the elixir version [required]
otp-version: ${{matrix.erlang}} # Define the OTP version [required]
+ experimental-otp: true # More info https://github.com/actions/setup-elixir/issues/31
- name: Retrieve Mix Dependencies Cache
uses: actions/cache@v2
@@ -98,7 +103,9 @@ jobs:
path: _build
key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix-deps-compile-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
+ - name: Docker-compose up
+ run: ./scripts/docker_up.sh
- name: Docker ps
- run: docker ps
+ run: docker ps -a
- name: Run Tests
run: ${{ matrix.tests }}

license: MIT License

original_message:
Update workflows to work with OTP

- Lock ubuntu version to 20.04
- Steps names to make it more explicit
- Fix OTP


message: Update to confluent kafka

diff:
@@ -66,16 +66,6 @@ jobs:
strategy:
fail-fast: false
matrix:
- tests:
- - mix test --include integration
- - mix test --include consumer_group
- - mix test --include server_0_p_10_and_later
- - mix test --include server_0_p_10_p_1
- - mix test --include server_0_p_9_p_0
- - mix test --include server_0_p_8_p_0
- - mix test --include new_client
- - mix test
-
include:
- elixir: 1.12.3
erlang: 24.3
@@ -108,4 +98,4 @@ jobs:
- name: Docker ps
run: docker ps -a
- name: Run Tests
- run: ${{ matrix.tests }}
+ run: ./scripts/all_tests.sh

diff:
@@ -14,9 +14,9 @@ config :kafka_ex,
# server.properties file.
# In the case below you would set "advertised.host.name=localhost"
brokers: [
+ {"localhost", 9092},
{"localhost", 9093},
- {"localhost", 9094},
- {"localhost", 9095}
+ {"localhost", 9094}
],
#
# OR:
@@ -61,6 +61,8 @@ config :kafka_ex,
# see SSL OPTION DESCRIPTIONS - CLIENT SIDE at http://erlang.org/doc/man/ssl.html
# for supported options
ssl_options: [
+ # Fix warnings. More at https://github.com/erlang/otp/issues/5352
+ verify: :verify_none,
cacertfile: File.cwd!() <> "/ssl/ca-cert",
certfile: File.cwd!() <> "/ssl/cert.pem",
keyfile: File.cwd!() <> "/ssl/key.pem"

diff:
-
#################################
# Common Kafka config
#################################
# Note: any property `something.bla` can be configure by setting `KAFKA_SOMETHING_BLA`.
-######## topic creation
-
-KAFKA_CREATE_TOPICS=consumer_group_implementation_test:4:2,test0p8p0:4:2
-
######## zookeeper
KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181
@@ -17,19 +12,17 @@ KAFKA_ZOOKEEPER_CONNECTION_TIMEOUT_MS=6000
######## advertised hosts names and protocols
# alternative to KAFKA_ADVERTISED_HOST_NAME is: HOSTNAME_COMMAND: ip addr | grep -o "inet [0-9.]*" | grep -v "127\.0\.0\.1" | grep -o "[0-9.]*"
-KAFKA_ADVERTISED_HOST_NAME=localhost
-KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=INSIDE:PLAINTEXT,OUTSIDE:SSL
-KAFKA_ADVERTISED_PROTOCOL_NAME=OUTSIDE
-KAFKA_PROTOCOL_NAME=INSIDE
+KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=INTERNAL:PLAINTEXT,EXTERNAL:SSL
+KAFKA_INTER_BROKER_LISTENER_NAME=INTERNAL
######## SSL
-KAFKA_SSL_KEYSTORE_LOCATION=/ssl/server.keystore.jks
+KAFKA_SSL_KEYSTORE_LOCATION=/etc/kafka/secrets/kafka.server.keystore.jks
KAFKA_SSL_KEYSTORE_PASSWORD=kafka_ex
KAFKA_SSL_KEY_PASSWORD=kafka_ex
-KAFKA_SSL_TRUSTSTORE_LOCATION=/ssl/server.truststore.jks
+
+KAFKA_SSL_TRUSTSTORE_LOCATION=/etc/kafka/secrets/kafka.server.truststore.jks
KAFKA_SSL_TRUSTSTORE_PASSWORD=kafka_ex
-KAFKA_SSL_SECURE_RANDOM_IMPLEMENTATION=SHA1PRNG
######## Config
@@ -37,7 +30,7 @@ KAFKA_DELETE_TOPIC_ENABLE=true
KAFKA_NUM_NETWORK_THREADS=1
KAFKA_NUM_IO_THREADS=1
KAFKA_BACKGROUND_THREADS=1
-KAFKA_LOG_CLEANER_THREADS=0
+KAFKA_LOG_CLEANER_THREADS=1
# KAFKA_SOCKET_SEND_BUFFER_BYTES=102400
# KAFKA_SOCKET_RECEIVE_BUFFER_BYTES=102400
# KAFKA_SOCKET_REQUEST_MAX_BYTES=104857600

diff:
-version: '2.4'
+version: "3.8"
services:
zookeeper:
- image: wurstmeister/zookeeper:3.4.6
+ image: confluentinc/cp-zookeeper:5.5.1
ports:
- - "2181:2181"
- healthcheck:
- test: ["CMD-SHELL", "echo ruok | nc -w 2 127.0.0.1 2181 | grep -q imok"]
- interval: 5s
- timeout: 10s
- retries: 15
- start_period: 10s
+ - '32181:32181'
+ environment:
+ ZOOKEEPER_CLIENT_PORT: 32181
+ ZOOKEEPER_TICK_TIME: 2000
- kafka1:
- hostname: kafka1
- image: wurstmeister/kafka:0.11.0.1
+ kafka-1:
+ image: confluentinc/cp-kafka:5.5.1
ports:
- - "9093:9093"
- volumes:
- - ./ssl:/ssl
- - ./scripts/kafka_check_health:/kafka_check_health
+ - '9092:9092'
+ depends_on:
+ - zookeeper
env_file: docker-compose-kafka.env
environment:
KAFKA_BROKER_ID: 1
- KAFKA_ADVERTISED_PORT: 9093
- KAFKA_PORT: 9092
- KAFKA_HOST_NAME: kafka1
+ KAFKA_ZOOKEEPER_CONNECT: zookeeper:32181
+ KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka-1:29092,EXTERNAL://localhost:9092
healthcheck:
- test: /kafka_check_health
- interval: 10s
+ test: kafka-topics --bootstrap-server kafka-1:29092 --list
+ interval: 30s
timeout: 10s
- retries: 30
- start_period: 10s
- depends_on:
- zookeeper:
- condition: service_healthy
+ retries: 4
+ volumes:
+ - ./ssl/kafka.server.keystore.jks:/etc/kafka/secrets/kafka.server.keystore.jks:ro,z
+ - ./ssl/kafka.server.truststore.jks:/etc/kafka/secrets/kafka.server.truststore.jks:ro,z
- kafka2:
- hostname: kafka2
- image: wurstmeister/kafka:0.11.0.1
+ kafka-2:
+ image: confluentinc/cp-kafka:5.5.1
ports:
- - "9094:9094"
- volumes:
- - ./ssl:/ssl
- - ./scripts/kafka_check_health:/kafka_check_health
+ - '9093:9093'
+ depends_on:
+ - zookeeper
env_file: docker-compose-kafka.env
environment:
KAFKA_BROKER_ID: 2
- KAFKA_ADVERTISED_PORT: 9094
- KAFKA_PORT: 9092
- KAFKA_HOST_NAME: kafka2
+ KAFKA_ZOOKEEPER_CONNECT: zookeeper:32181
+ KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka-2:29093,EXTERNAL://localhost:9093
healthcheck:
- test: /kafka_check_health
- interval: 10s
+ test: kafka-topics --bootstrap-server kafka-2:29093 --list
+ interval: 30s
timeout: 10s
- retries: 30
- start_period: 10s
- depends_on:
- zookeeper:
- condition: service_healthy
+ retries: 4
+ volumes:
+ - ./ssl/kafka.server.keystore.jks:/etc/kafka/secrets/kafka.server.keystore.jks:ro,z
+ - ./ssl/kafka.server.truststore.jks:/etc/kafka/secrets/kafka.server.truststore.jks:ro,z
- kafka3:
- hostname: kafka3
- image: wurstmeister/kafka:0.11.0.1
+ kafka-3:
+ image: confluentinc/cp-kafka:5.5.1
ports:
- - "9095:9095"
- volumes:
- - ./ssl:/ssl
- - ./scripts/kafka_check_health:/kafka_check_health
+ - '9094:9094'
+ depends_on:
+ - zookeeper
env_file: docker-compose-kafka.env
environment:
KAFKA_BROKER_ID: 3
- KAFKA_ADVERTISED_PORT: 9095
- KAFKA_PORT: 9092
- KAFKA_HOST_NAME: kafka3
+ KAFKA_ZOOKEEPER_CONNECT: zookeeper:32181
+ KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka-3:29094,EXTERNAL://localhost:9094
healthcheck:
- test: /kafka_check_health
- interval: 10s
+ test: kafka-topics --bootstrap-server kafka-2:29093 --list
+ interval: 30s
timeout: 10s
- retries: 30
- start_period: 10s
- depends_on:
- zookeeper:
- condition: service_healthy
+ retries: 4
+ volumes:
+ - ./ssl/kafka.server.keystore.jks:/etc/kafka/secrets/kafka.server.keystore.jks:ro,z
+ - ./ssl/kafka.server.truststore.jks:/etc/kafka/secrets/kafka.server.truststore.jks:ro,z
- # This is a dummy service that forces all other services to be healthy before
- # docker-compose up can be considered successful.
ready:
image: busybox:1.31-musl
command: tail -f /dev/null
depends_on:
- kafka1:
+ kafka-1:
condition: service_healthy
- kafka2:
+ kafka-2:
condition: service_healthy
- kafka3:
+ kafka-3:
condition: service_healthy
\ No newline at end of file

change_type: DELETE

diff:
-#! /bin/bash
-
-cd $(dirname $0)
-
-broker_ready() {
- # Inspired by: https://github.com/wurstmeister/kafka-docker/issues/167#issuecomment-439849789
- current_broker=$(cat $KAFKA_HOME/config/server.properties | awk 'BEGIN{FS="="}/^broker.id=/{print $2}')
- brokers=$($KAFKA_HOME/bin/zookeeper-shell.sh zookeeper:2181 <<< "ls /brokers/ids" | tail -1)
-
- echo "brokers: $brokers"
- echo "current_broker: $current_broker"
-
- echo "${brokers}" | jq '.[]' | grep -q "^${current_broker}$";
- return $?
-}
-
-#######################
-
-broker_ready
-
-#######################

change_type: RENAME

original_message: Update to confluent kafka


message:
MInor fixes
Update ssl related code and nest it into describes
Updates tests to support new broker version
Update docker-compose to create topics
fix check

diff:
@@ -31,39 +31,43 @@ jobs:
elixir-version: ${{matrix.elixir}} # Define the elixir version [required]
otp-version: ${{matrix.erlang}} # Define the OTP version [required]
experimental-otp: true # More info https://github.com/actions/setup-elixir/issues/31
- - name: Retrieve cached dependencies
+
+ - name: Retrieve Mix Dependencies Cache
uses: actions/cache@v2
- id: mix-cache
+ id: mix-cache # id to use in retrieve action
with:
- path: |
- deps
- _build
- key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('mix.lock') }}
- - name: Install dependencies
+ path: deps
+ key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
+
+ - name: Retrieve Mix Dependencies Compilation Cache
+ uses: actions/cache@v2
+ id: mix-deps-compile-cache # id to use in retrieve action
+ with:
+ path: _build
+ key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix-deps-compile-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
+ - name: Install Mix Dependencies
if: steps.mix-cache.outputs.cache-hit != 'true'
run: |
mix local.rebar --force
mix local.hex --force
mix deps.get
- mix deps.compile
+
+ - name: Compile Mix Dependencies
+ if: steps.mix-deps-compile-cache.outputs.cache-hit != 'true'
+ run: mix deps.compile
run-checks:
name: check
runs-on: ubuntu-20.04
- needs: [setup]
+ needs: [dependencies]
env:
MIX_ENV: dev
strategy:
fail-fast: false
matrix:
- include:
- - elixir: 1.10.2
- erlang: 22.3
- checks:
- - mix credo
- - mix compile --warnings-as-errors --force
- - mix format --check-formatted
+ elixir: ['1.12.3']
+ erlang: ['24.3']
steps:
- uses: actions/checkout@v2
@@ -88,20 +92,22 @@ jobs:
path: _build
key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix-deps-compile-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
- - run: ${{ matrix.checks }}
+ - run: |
+ mix compile --force
+ mix credo
+ mix format --check-formatted
dialyzer:
name: check / dialyzer
- needs: [setup]
+ needs: [dependencies]
runs-on: ubuntu-latest
env:
MIX_ENV: dev
strategy:
matrix:
- include:
- - elixir: 1.10.2
- erlang: 22.3
+ elixir: ['1.12.3']
+ erlang: ['24.3']
steps:
- uses: actions/checkout@v2

diff:
@@ -69,9 +69,8 @@ services:
- ./ssl/kafka.server.keystore.jks:/etc/kafka/secrets/kafka.server.keystore.jks:ro,z
- ./ssl/kafka.server.truststore.jks:/etc/kafka/secrets/kafka.server.truststore.jks:ro,z
- ready:
- image: busybox:1.31-musl
- command: tail -f /dev/null
+ kafka_setup:
+ image: confluentinc/cp-kafka:5.5.1
depends_on:
kafka-1:
condition: service_healthy
@@ -79,3 +78,6 @@ services:
condition: service_healthy
kafka-3:
condition: service_healthy
+ command: "bash -c 'kafka-topics --zookeeper zookeeper:32181 --create --if-not-exists --partitions 4 --replication-factor 2 --topic consumer_group_implementation_test && \
+ kafka-topics --zookeeper zookeeper:32181 --create --if-not-exists --partitions 4 --replication-factor 2 --topic test0p8p0 && \
+ kafka-topics --zookeeper zookeeper:32181 --list'"

diff:
@@ -810,9 +810,7 @@ defmodule KafkaEx.GenConsumer do
KafkaEx.latest_offset(topic, partition, worker_name)
_ ->
- raise "Offset out of range while consuming topic #{topic}, partition #{
- partition
- }."
+ raise "Offset out of range while consuming topic #{topic}, partition #{partition}."
end
%State{

diff:
@@ -25,9 +25,7 @@ defmodule KafkaEx.NetworkClient do
err ->
Logger.log(
:error,
- "Could not connect to broker #{inspect(host)}:#{inspect(port)} because of error #{
- inspect(err)
- }"
+ "Could not connect to broker #{inspect(host)}:#{inspect(port)} because of error #{inspect(err)}"
)
nil
@@ -50,9 +48,7 @@ defmodule KafkaEx.NetworkClient do
{_, reason} ->
Logger.log(
:error,
- "Asynchronously sending data to broker #{inspect(broker.host)}:#{
- inspect(broker.port)
- } failed with #{inspect(reason)}"
+ "Asynchronously sending data to broker #{inspect(broker.host)}:#{inspect(broker.port)} failed with #{inspect(reason)}"
)
reason
@@ -77,9 +73,7 @@ defmodule KafkaEx.NetworkClient do
{:error, reason} ->
Logger.log(
:error,
- "Receiving data from broker #{inspect(broker.host)}:#{
- inspect(broker.port)
- } failed with #{inspect(reason)}"
+ "Receiving data from broker #{inspect(broker.host)}:#{inspect(broker.port)} failed with #{inspect(reason)}"
)
Socket.close(socket)
@@ -90,9 +84,7 @@ defmodule KafkaEx.NetworkClient do
{_, reason} ->
Logger.log(
:error,
- "Sending data to broker #{inspect(broker.host)}:#{
- inspect(broker.port)
- } failed with #{inspect(reason)}"
+ "Sending data to broker #{inspect(broker.host)}:#{inspect(broker.port)} failed with #{inspect(reason)}"
)
Socket.close(socket)

diff:
@@ -162,9 +162,7 @@ defmodule KafkaEx.Protocol.Fetch do
)
when byte_size(partial_message_data) < msg_size do
raise RuntimeError,
- "Insufficient data fetched at offset #{offset}. Message size is #{
- msg_size
- } but only received #{byte_size(partial_message_data)} bytes. Try increasing max_bytes."
+ "Insufficient data fetched at offset #{offset}. Message size is #{msg_size} but only received #{byte_size(partial_message_data)} bytes. Try increasing max_bytes."
end
# handles the single message case and the batch (compression) case

diff:
@@ -373,9 +373,7 @@ defmodule KafkaEx.Server do
def terminate(reason, state) do
Logger.log(
:debug,
- "Shutting down worker #{inspect(state.worker_name)}, reason: #{
- inspect(reason)
- }"
+ "Shutting down worker #{inspect(state.worker_name)}, reason: #{inspect(reason)}"
)
if state.event_pid do
@@ -471,9 +469,7 @@ defmodule KafkaEx.Server do
nil ->
Logger.log(
:error,
- "kafka_server_produce_send_request: leader for topic #{
- produce_request.topic
- }/#{produce_request.partition} is not available"
+ "kafka_server_produce_send_request: leader for topic #{produce_request.topic}/#{produce_request.partition} is not available"
)
:leader_not_available
@@ -678,9 +674,7 @@ defmodule KafkaEx.Server do
) do
Logger.log(
:error,
- "Metadata request for topic #{inspect(topic)} failed with error_code #{
- inspect(error_code)
- }"
+ "Metadata request for topic #{inspect(topic)} failed with error_code #{inspect(error_code)}"
)
{correlation_id, %Metadata.Response{}}
@@ -731,9 +725,7 @@ defmodule KafkaEx.Server do
end
else
message =
- "Unable to fetch metadata from any brokers. Timeout is #{
- sync_timeout
- }."
+ "Unable to fetch metadata from any brokers. Timeout is #{sync_timeout}."
Logger.log(:error, message)
raise message
@@ -862,9 +854,7 @@ defmodule KafkaEx.Server do
case broker do
nil ->
Logger.error(fn ->
- "network_request: leader for topic #{request.topic}/#{
- request.partition
- } is not available"
+ "network_request: leader for topic #{request.topic}/#{request.partition} is not available"
end)
{{:error, :topic_not_found}, updated_state}
@@ -891,15 +881,11 @@ defmodule KafkaEx.Server do
rescue
_ ->
Logger.error(
- "Failed to parse a response from the server: #{
- inspect(response)
- }"
+ "Failed to parse a response from the server: #{inspect(response)}"
)
Kernel.reraise(
- "Parse error during #{inspect(module)}.parse_response. Couldn't parse: #{
- inspect(response)
- }",
+ "Parse error during #{inspect(module)}.parse_response. Couldn't parse: #{inspect(response)}",
System.stacktrace()
)
end
@@ -932,9 +918,7 @@ defmodule KafkaEx.Server do
Enum.each(brokers_to_remove, fn broker ->
Logger.log(
:debug,
- "Closing connection to broker #{broker.node_id}: #{
- inspect(broker.host)
- } on port #{inspect(broker.port)}"
+ "Closing connection to broker #{broker.node_id}: #{inspect(broker.host)} on port #{inspect(broker.port)}"
)
NetworkClient.close_socket(broker.socket)
@@ -956,9 +940,7 @@ defmodule KafkaEx.Server do
nil ->
Logger.log(
:debug,
- "Establishing connection to broker #{metadata_broker.node_id}: #{
- inspect(metadata_broker.host)
- } on port #{inspect(metadata_broker.port)}"
+ "Establishing connection to broker #{metadata_broker.node_id}: #{inspect(metadata_broker.host)} on port #{inspect(metadata_broker.port)}"
)
add_new_brokers(

diff:
@@ -226,9 +226,7 @@ defmodule KafkaEx.Server0P8P2 do
) do
Logger.log(
:error,
- "Fetching consumer_group #{consumer_group} metadata failed with error_code #{
- inspect(error_code)
- }"
+ "Fetching consumer_group #{consumer_group} metadata failed with error_code #{inspect(error_code)}"
)
{%ConsumerMetadataResponse{error_code: error_code}, state}

diff:
@@ -42,7 +42,7 @@ defmodule KafkaEx.Mixfile do
main_deps = [
{:kayrock, "~> 0.1.12"},
{:credo, "~> 1.1", only: [:dev, :test], runtime: false},
- {:dialyxir, "~> 1.0.0-rc.3", only: :dev, runtime: false},
+ {:dialyxir, "~> 1.0", only: :dev, runtime: false},
{:excoveralls, "~> 0.7", only: :test, runtime: false},
{:snappy,
git: "https://github.com/fdmanana/snappy-erlang-nif", only: [:dev, :test]}

diff:
@@ -36,6 +36,7 @@ defmodule KafkaEx.Socket.Test do
{:ok, listen_socket} =
:ssl.listen(port, [
:binary,
+ {:verify, :verify_none},
{:active, false},
{:reuseaddr, true},
{:packet, 0},
@@ -49,7 +50,7 @@ defmodule KafkaEx.Socket.Test do
defp listen(socket) do
case :ssl.transport_accept(socket) do
{:ok, conn} ->
- :ok = :ssl.ssl_accept(conn)
+ {:ok, _socket} = :ssl.handshake(conn)
pid = spawn_link(fn -> recv(conn) end)
:ssl.controlling_process(socket, pid)
@@ -73,9 +74,12 @@ defmodule KafkaEx.Socket.Test do
setup_all do
:ssl.start()
- SSLServer.start(3030)
+ end
+
+ describe "without SSL socket" do
+ setup do
Server.start(3040)
- {:ok, [ssl_port: 3030, port: 3040]}
+ {:ok, [port: 3040]}
end
test "create a non SSL socket", context do
@@ -119,6 +123,13 @@ defmodule KafkaEx.Socket.Test do
KafkaEx.Socket.close(socket)
assert {:error, :closed} == KafkaEx.Socket.send(socket, 'ping')
end
+ end
+
+ describe "with ssl socket" do
+ setup do
+ SSLServer.start(3030)
+ {:ok, [ssl_port: 3030]}
+ end
test "create a SSL socket", context do
{:ok, socket} =
@@ -162,3 +173,4 @@ defmodule KafkaEx.Socket.Test do
assert {:error, :closed} == KafkaEx.Socket.send(socket, 'ping')
end
end
+end

original_message:
MInor fixes

- Update ssl related code and nest it into describes
- Updates tests to support new broker version
- Update docker-compose to create topics
- fix check


message: MIx format & fix dialyxir

diff:
@@ -116,6 +116,8 @@ jobs:
with:
elixir-version: ${{matrix.elixir}} # Define the elixir version [required]
otp-version: ${{matrix.erlang}} # Define the OTP version [required]
+ experimental-otp: true # More info https://github.com/actions/setup-elixir/issues/31
+
- name: Retrieve Mix Dependencies Cache
uses: actions/cache@v2
id: mix-cache # id to use in retrieve action

diff:
@@ -297,11 +297,12 @@ defmodule KafkaEx do
api_version = Keyword.get(opts, :api_version, 0)
# same for offset_commit_api_version
offset_commit_api_version = Keyword.get(opts, :offset_commit_api_version, 0)
+
# By default, it makes sense to synchronize the API version of the offset_commit and the offset_fetch
# operations, otherwise we might commit the offsets in zookeeper and read them from Kafka, meaning
# that the value would be incorrect.
- offset_fetch_api_version = Keyword.get(opts, :offset_fetch_api_version, offset_commit_api_version)
-
+ offset_fetch_api_version =
+ Keyword.get(opts, :offset_fetch_api_version, offset_commit_api_version)
retrieved_offset =
current_offset(

original_message: MIx format & fix dialyxir


message:
Apply mix format
Apply mix format to update code quality before enabling github actions checks.

diff:
@@ -164,9 +164,7 @@ defmodule KafkaEx.Protocol.Fetch do
)
when byte_size(partial_message_data) < msg_size do
raise RuntimeError,
- "Insufficient data fetched at offset #{offset}. Message size is #{
- msg_size
- } but only received #{byte_size(partial_message_data)} bytes. Try increasing max_bytes."
+ "Insufficient data fetched at offset #{offset}. Message size is #{msg_size} but only received #{byte_size(partial_message_data)} bytes. Try increasing max_bytes."
end
# handles the single message case and the batch (compression) case

diff:
@@ -373,9 +373,7 @@ defmodule KafkaEx.Server do
def terminate(reason, state) do
Logger.log(
:debug,
- "Shutting down worker #{inspect(state.worker_name)}, reason: #{
- inspect(reason)
- }"
+ "Shutting down worker #{inspect(state.worker_name)}, reason: #{inspect(reason)}"
)
if state.event_pid do
@@ -471,9 +469,7 @@ defmodule KafkaEx.Server do
nil ->
Logger.log(
:error,
- "kafka_server_produce_send_request: leader for topic #{
- produce_request.topic
- }/#{produce_request.partition} is not available"
+ "kafka_server_produce_send_request: leader for topic #{produce_request.topic}/#{produce_request.partition} is not available"
)
:leader_not_available
@@ -682,9 +678,7 @@ defmodule KafkaEx.Server do
) do
Logger.log(
:error,
- "Metadata request for topic #{inspect(topic)} failed with error_code #{
- inspect(error_code)
- }"
+ "Metadata request for topic #{inspect(topic)} failed with error_code #{inspect(error_code)}"
)
{correlation_id, %Metadata.Response{}}
@@ -735,9 +729,7 @@ defmodule KafkaEx.Server do
end
else
message =
- "Unable to fetch metadata from any brokers. Timeout is #{
- sync_timeout
- }."
+ "Unable to fetch metadata from any brokers. Timeout is #{sync_timeout}."
Logger.log(:error, message)
raise message
@@ -866,9 +858,7 @@ defmodule KafkaEx.Server do
case broker do
nil ->
Logger.error(fn ->
- "network_request: leader for topic #{request.topic}/#{
- request.partition
- } is not available"
+ "network_request: leader for topic #{request.topic}/#{request.partition} is not available"
end)
{{:error, :topic_not_found}, updated_state}
@@ -895,15 +885,11 @@ defmodule KafkaEx.Server do
rescue
_ ->
Logger.error(
- "Failed to parse a response from the server: #{
- inspect(response)
- }"
+ "Failed to parse a response from the server: #{inspect(response)}"
)
Kernel.reraise(
- "Parse error during #{inspect(module)}.parse_response. Couldn't parse: #{
- inspect(response)
- }",
+ "Parse error during #{inspect(module)}.parse_response. Couldn't parse: #{inspect(response)}",
System.stacktrace()
)
end
@@ -936,9 +922,7 @@ defmodule KafkaEx.Server do
Enum.each(brokers_to_remove, fn broker ->
Logger.log(
:debug,
- "Closing connection to broker #{broker.node_id}: #{
- inspect(broker.host)
- } on port #{inspect(broker.port)}"
+ "Closing connection to broker #{broker.node_id}: #{inspect(broker.host)} on port #{inspect(broker.port)}"
)
NetworkClient.close_socket(broker.socket)
@@ -960,9 +944,7 @@ defmodule KafkaEx.Server do
nil ->
Logger.log(
:debug,
- "Establishing connection to broker #{metadata_broker.node_id}: #{
- inspect(metadata_broker.host)
- } on port #{inspect(metadata_broker.port)}"
+ "Establishing connection to broker #{metadata_broker.node_id}: #{inspect(metadata_broker.host)} on port #{inspect(metadata_broker.port)}"
)
add_new_brokers(


message:
Add more otp & elixir versions to matrix
Support multiple otp & elixir version for tests
Support only newst version for static check.
Handle backward compability in tests

diff:
@@ -15,8 +15,8 @@ jobs:
ACTIONS_ALLOW_UNSECURE_COMMANDS: true
strategy:
matrix:
- elixir: ['1.12.3']
- erlang: ['24.3']
+ elixir: ['1.12']
+ otp: ['24.3']
steps:
- name: Cancel previous runs
@@ -31,7 +31,7 @@ jobs:
uses: actions/setup-elixir@v1
with:
elixir-version: ${{matrix.elixir}} # Define the elixir version [required]
- otp-version: ${{matrix.erlang}} # Define the OTP version [required]
+ otp-version: ${{matrix.otp}} # Define the OTP version [required]
experimental-otp: true # More info https://github.com/actions/setup-elixir/issues/31
- name: Retrieve Cached Dependencies
@@ -64,8 +64,8 @@ jobs:
strategy:
fail-fast: false
matrix:
- elixir: ['1.12.3']
- erlang: ['24.3']
+ elixir: ['1.12']
+ otp: ['24.3']
steps:
- name: Cancel Previous Runs
@@ -82,7 +82,7 @@ jobs:
uses: actions/setup-elixir@v1
with:
elixir-version: ${{matrix.elixir}} # Define the elixir version [required]
- otp-version: ${{matrix.erlang}} # Define the OTP version [required]
+ otp-version: ${{matrix.otp}} # Define the OTP version [required]
experimental-otp: true # More info https://github.com/actions/setup-elixir/issues/31
- name: Retrieve Cached Dependencies

diff:
@@ -14,8 +14,13 @@ jobs:
MIX_ENV: test
strategy:
matrix:
- elixir: ['1.12.3']
- erlang: ['24.3']
+ pair:
+ - elixir: 1.13
+ otp: 24.3
+ - elixir: 1.11
+ otp: 21.3
+ - elixir: 1.6
+ otp: 20.3
steps:
- name: Cancel previous runs
@@ -27,8 +32,8 @@ jobs:
- name: Setup elixir & erlang enviroment
uses: actions/setup-elixir@v1
with:
- elixir-version: ${{matrix.elixir}} # Define the elixir version [required]
- otp-version: ${{matrix.erlang}} # Define the OTP version [required]
+ elixir-version: ${{matrix.pair.elixir}} # Define the elixir version [required]
+ otp-version: ${{matrix.pair.otp}} # Define the OTP version [required]
experimental-otp: true # More info https://github.com/actions/setup-elixir/issues/31
- name: Retrieve Mix Dependencies Cache
@@ -36,14 +41,14 @@ jobs:
id: mix-cache # id to use in retrieve action
with:
path: deps
- key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
+ key: ${{ runner.os }}-${{ matrix.pair.otp }}-${{ matrix.pair.elixir }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
- name: Retrieve Mix Dependencies Compilation Cache
uses: actions/cache@v2
id: mix-deps-compile-cache # id to use in retrieve action
with:
path: _build
- key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix-deps-compile-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
+ key: ${{ runner.os }}-${{ matrix.pair.otp }}-${{ matrix.pair.elixir }}-mix-deps-compile-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
- name: Install Mix Dependencies
if: steps.mix-cache.outputs.cache-hit != 'true'
run: |
@@ -66,17 +71,21 @@ jobs:
strategy:
fail-fast: false
matrix:
- include:
- - elixir: 1.12.3
- erlang: 24.3
+ pair:
+ - elixir: 1.13
+ otp: 24.3
+ - elixir: 1.11
+ otp: 21.3
+ - elixir: 1.6
+ otp: 20.3
steps:
- uses: actions/checkout@v2
- name: Setup elixir & erlang enviroment
uses: actions/setup-elixir@v1
with:
- elixir-version: ${{matrix.elixir}} # Define the elixir version [required]
- otp-version: ${{matrix.erlang}} # Define the OTP version [required]
+ elixir-version: ${{matrix.pair.elixir}} # Define the elixir version [required]
+ otp-version: ${{matrix.pair.otp}} # Define the OTP version [required]
experimental-otp: true # More info https://github.com/actions/setup-elixir/issues/31
- name: Retrieve Mix Dependencies Cache
@@ -84,14 +93,14 @@ jobs:
id: mix-cache # id to use in retrieve action
with:
path: deps
- key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
+ key: ${{ runner.os }}-${{ matrix.pair.otp }}-${{ matrix.pair.elixir }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
- name: Retrieve Mix Dependencies Compilation Cache
uses: actions/cache@v2
id: mix-deps-compile-cache # id to use in retrieve action
with:
path: _build
- key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-mix-deps-compile-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
+ key: ${{ runner.os }}-${{ matrix.pair.otp }}-${{ matrix.pair.elixir }}-mix-deps-compile-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
- name: Docker-compose up
run: ./scripts/docker_up.sh

diff:
@@ -50,7 +50,12 @@ defmodule KafkaEx.Socket.Test do
defp listen(socket) do
case :ssl.transport_accept(socket) do
{:ok, conn} ->
+ if opt_version_21_plus? do
{:ok, _socket} = :ssl.handshake(conn)
+ else
+ :ok = :ssl.ssl_accept(conn)
+ end
+
pid = spawn_link(fn -> recv(conn) end)
:ssl.controlling_process(socket, pid)
@@ -70,6 +75,11 @@ defmodule KafkaEx.Socket.Test do
:ok
end
end
+
+ defp opt_version_21_plus? do
+ {version, _} = System.otp_release() |> Float.parse()
+ version >= 21
+ end
end
setup_all do


message: Fix syntax in socket tests

diff:
@@ -50,7 +50,7 @@ defmodule KafkaEx.Socket.Test do
defp listen(socket) do
case :ssl.transport_accept(socket) do
{:ok, conn} ->
- if opt_version_21_plus? do
+ if opt_version_21_plus?() do
{:ok, _socket} = :ssl.handshake(conn)
else
:ok = :ssl.ssl_accept(conn)

original_message: Fix syntax in socket tests


message:
Add more specific versions for elixir & otp
As we are using a cache for plt generation we have to have more specific cache keys

diff:
@@ -15,8 +15,8 @@ jobs:
ACTIONS_ALLOW_UNSECURE_COMMANDS: true
strategy:
matrix:
- elixir: ['1.12']
- otp: ['24.3']
+ elixir: ['1.12.3']
+ otp: ['24.3.4']
steps:
- name: Cancel previous runs
@@ -64,8 +64,8 @@ jobs:
strategy:
fail-fast: false
matrix:
- elixir: ['1.12']
- otp: ['24.3']
+ elixir: ['1.12.3']
+ otp: ['24.3.4']
steps:
- name: Cancel Previous Runs


message: Apply minimum rules

change_type: ADD

diff:
+disabled_rules:
+ - line_length
+ - trailing_whitespace
+
+included:
+ - DroidKaigi 2019
+
+excluded:
+ - Carthage
+ - Pods
\ No newline at end of file

diff:
@@ -13,7 +13,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
-
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
setupNavigationBar()
return true

diff:
import UIKit
+// swiftlint:disable identifier_name
+
extension UIColor {
enum DK: String {
@@ -39,3 +41,5 @@ extension UIColor {
self.init(hex: hex, alpha: 1.0)
}
}
+
+// swiftlint:enable identifier_name

diff:
import UIKit
-
// MARK: - Reuse Extension
extension UITableView {

diff:
import Foundation
import UIKit
+// swiftlint:disable force_cast
+
protocol NibLoadable: class {
static var nibName: String { get }
}
@@ -22,3 +24,5 @@ extension NibLoadable where Self: UIView {
return nib.instantiate(withOwner: self, options: nil).first as! Self
}
}
+
+// swiftlint:enable force_cast

diff:
import Foundation
import UIKit
-
protocol StoryboardInstantiable {
static var storyboardName: String { get }
}

diff:
@@ -128,7 +128,6 @@ class SessionTableViewCell: UITableViewCell, Reusable {
return collectionView
}()
-
private func setupSubviews() {
[titleLabel, liveMark, speakersStackView, timeAndRoomLabel, collectionView].forEach(contentView.addSubview)
titleLabel.snp.makeConstraints {
@@ -184,7 +183,6 @@ extension SessionTableViewCell: UICollectionViewDataSource {
}
}
-
final class SessionCalculateHeightTableViewCell: UITableViewCell {
var session: Session? {
@@ -202,14 +200,12 @@ final class SessionCalculateHeightTableViewCell: UITableViewCell {
}
}
-
override init(style: CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupSubviews()
}
required init?(coder aDecoder: NSCoder) { fatalError() }
-
private var tagContents: [TagContent] = []
lazy var collectionView: UICollectionView = {

license: Apache License 2.0

original_message: Apply minimum rules


message: Fix max width of tags

diff:
@@ -83,7 +83,7 @@ class TagsCollectionViewCell: UICollectionViewCell, Reusable {
label.padding = UIEdgeInsets(top: 6, left: 10, bottom: 6, right: 10)
label.font = UIFont.systemFont(ofSize: 12)
label.numberOfLines = 1
- label.textAlignment = .center
+ label.textAlignment = .left
label.lineBreakMode = .byTruncatingTail
label.clipsToBounds = true
return label
@@ -93,7 +93,6 @@ class TagsCollectionViewCell: UICollectionViewCell, Reusable {
addSubview(label)
label.snp.makeConstraints {
$0.edges.equalToSuperview()
- $0.width.lessThanOrEqualTo(230)
}
}
}

diff:
@@ -20,6 +20,13 @@ final class TagsLeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout
layoutAttribute.frame.origin.x = leftMargin
leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
maxY = max(layoutAttribute.frame.maxY, maxY)
+
+ if let collectionView = collectionView {
+ let bounds = collectionView.bounds
+ let contentInset = collectionView.contentInset
+ let maxWidth = bounds.width - contentInset.left - contentInset.right
+ layoutAttribute.frame.size.width = min(maxWidth, layoutAttribute.bounds.width)
+ }
}
return attributes
}

original_message: Fix max width of tags


message: Scroll to the current session when a Day tab is reselected

diff:
@@ -9,6 +9,7 @@ import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.transition.AutoTransition
import androidx.transition.TransitionManager
+import com.shopify.livedataktx.observe
import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.Item
import com.xwray.groupie.databinding.ViewHolder
@@ -143,6 +144,11 @@ class BottomSheetDaySessionsFragment : DaggerFragment() {
binding.isCollapsed = isCollapsed
}
}
+ sessionPagesStore.reselectedTab.observe(viewLifecycleOwner) {
+ it?.let { position ->
+ binding.sessionsRecycler.scrollToPosition(position)
+ }
+ }
}
override fun onDestroyView() {

diff:
@@ -13,6 +13,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentStatePagerAdapter
import androidx.lifecycle.Lifecycle
import androidx.viewpager.widget.ViewPager
+import com.google.android.material.tabs.TabLayout
import com.soywiz.klock.DateTime
import com.soywiz.klock.hours
import dagger.Module
@@ -120,6 +121,23 @@ class SessionPagesFragment : DaggerFragment() {
}
)
+ binding.sessionsTabLayout.addOnTabSelectedListener(
+ object : TabLayout.OnTabSelectedListener {
+ override fun onTabReselected(tab: TabLayout.Tab?) {
+ tab?.let {
+ (SessionPage.pages[it.position] as? SessionPage.Day)?.let { page ->
+ sessionPagesActionCreator.reselectTab(page.day)
+ }
+ }
+ }
+
+ override fun onTabUnselected(tab: TabLayout.Tab?) { // no-op
+ }
+
+ override fun onTabSelected(tab: TabLayout.Tab?) { // no-op
+ }
+ })
+
(0 until binding.sessionsTabLayout.tabCount).forEach { tabIndex ->
val tab = binding.sessionsTabLayout.getTabAt(tabIndex)
tab?.let {

diff:
@@ -30,6 +30,10 @@ class SessionPagesActionCreator @Inject constructor(
dispatcher.launchAndDispatch(Action.SessionPageSelected(sessionPage))
}
+ fun reselectTab(dayNumber: Int) {
+ dispatcher.launchAndDispatch(Action.SessionPageReselected(dayNumber))
+ }
+
fun changeFilter(room: Room, checked: Boolean) {
dispatcher.launchAndDispatch(Action.RoomFilterChanged(room, checked))
}

diff:
@@ -119,6 +119,14 @@ class SessionPagesStore @Inject constructor(
.map { it.sessionPage }
.toLiveData(SessionPage.pages[0])
+ val reselectedTab: LiveData<Int> = dispatcher
+ .subscribe<Action.SessionPageReselected>()
+ .map { action ->
+ filteredSessions.requireValue()
+ .filter { session -> session.dayNumber == action.dayNumber }
+ .indexOfFirst { session -> session.isOnGoing }
+ }.toLiveData(0)
+
fun filteredSessionsByDay(day: Int): LiveData<List<Session>> {
return filteredSessions
.map { sessions ->

diff:
@@ -31,6 +31,7 @@ sealed class Action {
) : Action()
data class SessionPageSelected(val sessionPage: SessionPage) : Action()
+ data class SessionPageReselected(val dayNumber: Int) : Action()
class SystemPropertyLoaded(val system: SystemProperty) : Action()
open class FilterChange<T>(val value: T, open val checked: Boolean) : Action()


message: Don't use requireValue. If value is null, screen will not scroll

diff:
@@ -122,10 +122,10 @@ class SessionPagesStore @Inject constructor(
val reselectedTab: LiveData<Int> = dispatcher
.subscribe<Action.SessionPageReselected>()
.map { action ->
- filteredSessions.requireValue()
- .filter { session -> session.dayNumber == action.dayNumber }
- .indexOfFirst { session -> session.isOnGoing }
- }.toLiveData(0)
+ filteredSessions.value
+ ?.filter { session -> session.dayNumber == action.dayNumber }
+ ?.indexOfFirst { session -> session.isOnGoing } ?: -1
+ }.toLiveData(-1)
fun filteredSessionsByDay(day: Int): LiveData<List<Session>> {
return filteredSessions


message: Insert `nonNull()`

diff:
@@ -9,6 +9,7 @@ import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.transition.AutoTransition
import androidx.transition.TransitionManager
+import com.shopify.livedataktx.nonNull
import com.shopify.livedataktx.observe
import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.Item
@@ -144,10 +145,8 @@ class BottomSheetDaySessionsFragment : DaggerFragment() {
binding.isCollapsed = isCollapsed
}
}
- sessionPagesStore.reselectedTab.observe(viewLifecycleOwner) {
- it?.let { position ->
- binding.sessionsRecycler.scrollToPosition(position)
- }
+ sessionPagesStore.reselectedTab.nonNull().observe(viewLifecycleOwner) {
+ binding.sessionsRecycler.scrollToPosition(it)
}
}

original_message: Insert `nonNull()`


message: Replace with toSingleLiveData()

diff:
@@ -10,6 +10,7 @@ import io.github.droidkaigi.confsched2019.action.Action
import io.github.droidkaigi.confsched2019.dispatcher.Dispatcher
import io.github.droidkaigi.confsched2019.ext.android.requireValue
import io.github.droidkaigi.confsched2019.ext.android.toLiveData
+import io.github.droidkaigi.confsched2019.ext.android.toSingleLiveData
import io.github.droidkaigi.confsched2019.model.Filters
import io.github.droidkaigi.confsched2019.model.Session
import io.github.droidkaigi.confsched2019.model.SessionPage
@@ -125,7 +126,7 @@ class SessionPagesStore @Inject constructor(
filteredSessions.value
?.filter { session -> session.dayNumber == action.dayNumber }
?.indexOfFirst { session -> session.isOnGoing } ?: -1
- }.toLiveData(-1)
+ }.toSingleLiveData(-1)
fun filteredSessionsByDay(day: Int): LiveData<List<Session>> {
return filteredSessions

original_message: Replace with toSingleLiveData()


message: Fix `When reselected tab then scroll succeeded, the other tab scroll too.`

diff:
@@ -145,7 +145,8 @@ class BottomSheetDaySessionsFragment : DaggerFragment() {
binding.isCollapsed = isCollapsed
}
}
- sessionPagesStore.reselectedTab.nonNull().observe(viewLifecycleOwner) {
+ sessionPagesStore.onGoingSessionOnTabReselected(args.day).nonNull()
+ .observe(viewLifecycleOwner) {
binding.sessionsRecycler.scrollToPosition(it)
}
}

diff:
@@ -115,19 +115,16 @@ class SessionPagesStore @Inject constructor(
}
}.map { it.orEmpty() }
+ private val reselectedTab: LiveData<Int> = dispatcher
+ .subscribe<Action.SessionPageReselected>()
+ .map { it.dayNumber }
+ .toSingleLiveData(0)
+
val selectedTab: LiveData<SessionPage> = dispatcher
.subscribe<Action.SessionPageSelected>()
.map { it.sessionPage }
.toLiveData(SessionPage.pages[0])
- val reselectedTab: LiveData<Int> = dispatcher
- .subscribe<Action.SessionPageReselected>()
- .map { action ->
- filteredSessions.value
- ?.filter { session -> session.dayNumber == action.dayNumber }
- ?.indexOfFirst { session -> session.isOnGoing } ?: -1
- }.toSingleLiveData(-1)
-
fun filteredSessionsByDay(day: Int): LiveData<List<Session>> {
return filteredSessions
.map { sessions ->
@@ -149,4 +146,16 @@ class SessionPagesStore @Inject constructor(
.filter { session -> session.isFavorited }
}
}
+
+ fun onGoingSessionOnTabReselected(day: Int): LiveData<Int> {
+ return reselectedTab
+ .map { reselectedDay ->
+ if (reselectedDay != day) -1
+ else {
+ filteredSessions.value
+ ?.filter { session -> session.dayNumber == reselectedDay }
+ ?.indexOfFirst { session -> session.isOnGoing } ?: -1
+ }
+ }
+ }
}


message: Use SessionPage instead of integer in SessionPageReselected

diff:
@@ -145,9 +145,10 @@ class BottomSheetDaySessionsFragment : DaggerFragment() {
binding.isCollapsed = isCollapsed
}
}
- sessionPagesStore.onGoingSessionOnTabReselected(args.day).nonNull()
- .observe(viewLifecycleOwner) {
- binding.sessionsRecycler.scrollToPosition(it)
+ sessionPagesStore.reselectedTab.observe(viewLifecycleOwner) {
+ if (SessionPage.pageOfDay(args.day) == it) {
+ scrollToCurrentSession(args.day)
+ }
}
}
@@ -156,6 +157,11 @@ class BottomSheetDaySessionsFragment : DaggerFragment() {
super.onDestroyView()
}
+ private fun scrollToCurrentSession(day: Int) {
+ val position = sessionPagesStore.onGoingSessionIndex(day)
+ binding.sessionsRecycler.scrollToPosition(position)
+ }
+
companion object {
fun newInstance(
args: BottomSheetDaySessionsFragmentArgs

diff:
@@ -125,9 +125,7 @@ class SessionPagesFragment : DaggerFragment() {
object : TabLayout.OnTabSelectedListener {
override fun onTabReselected(tab: TabLayout.Tab?) {
tab?.let {
- (SessionPage.pages[it.position] as? SessionPage.Day)?.let { page ->
- sessionPagesActionCreator.reselectTab(page.day)
- }
+ sessionPagesActionCreator.reselectTab(SessionPage.pages[it.position])
}
}

diff:
@@ -30,8 +30,8 @@ class SessionPagesActionCreator @Inject constructor(
dispatcher.launchAndDispatch(Action.SessionPageSelected(sessionPage))
}
- fun reselectTab(dayNumber: Int) {
- dispatcher.launchAndDispatch(Action.SessionPageReselected(dayNumber))
+ fun reselectTab(sessionPage: SessionPage) {
+ dispatcher.launchAndDispatch(Action.SessionPageReselected(sessionPage))
}
fun changeFilter(room: Room, checked: Boolean) {

diff:
@@ -115,16 +115,16 @@ class SessionPagesStore @Inject constructor(
}
}.map { it.orEmpty() }
- private val reselectedTab: LiveData<Int> = dispatcher
- .subscribe<Action.SessionPageReselected>()
- .map { it.dayNumber }
- .toSingleLiveData(0)
-
val selectedTab: LiveData<SessionPage> = dispatcher
.subscribe<Action.SessionPageSelected>()
.map { it.sessionPage }
.toLiveData(SessionPage.pages[0])
+ val reselectedTab: LiveData<SessionPage> = dispatcher
+ .subscribe<Action.SessionPageReselected>()
+ .map { it.sessionPage }
+ .toSingleLiveData(SessionPage.pages[0])
+
fun filteredSessionsByDay(day: Int): LiveData<List<Session>> {
return filteredSessions
.map { sessions ->
@@ -147,15 +147,7 @@ class SessionPagesStore @Inject constructor(
}
}
- fun onGoingSessionOnTabReselected(day: Int): LiveData<Int> {
- return reselectedTab
- .map { reselectedDay ->
- if (reselectedDay != day) -1
- else {
- filteredSessions.value
- ?.filter { session -> session.dayNumber == reselectedDay }
- ?.indexOfFirst { session -> session.isOnGoing } ?: -1
- }
- }
- }
+ fun onGoingSessionIndex(day: Int) = filteredSessions.value.orEmpty()
+ .filter { session -> session.dayNumber == day }
+ .indexOfFirst { session -> session.isOnGoing }
}

diff:
@@ -31,7 +31,7 @@ sealed class Action {
) : Action()
data class SessionPageSelected(val sessionPage: SessionPage) : Action()
- data class SessionPageReselected(val dayNumber: Int) : Action()
+ data class SessionPageReselected(val sessionPage: SessionPage) : Action()
class SystemPropertyLoaded(val system: SystemProperty) : Action()
open class FilterChange<T>(val value: T, open val checked: Boolean) : Action()


message: Move code of getting index to Fragment

diff:
@@ -146,7 +146,7 @@ class BottomSheetDaySessionsFragment : DaggerFragment() {
}
sessionPagesStore.reselectedTab.observe(viewLifecycleOwner) {
if (SessionPage.pageOfDay(args.day) == it) {
- scrollToCurrentSession(args.day)
+ scrollToCurrentSession()
}
}
}
@@ -156,8 +156,10 @@ class BottomSheetDaySessionsFragment : DaggerFragment() {
super.onDestroyView()
}
- private fun scrollToCurrentSession(day: Int) {
- val position = sessionPagesStore.onGoingSessionIndex(day)
+ private fun scrollToCurrentSession() {
+ val position = sessionPagesStore.filteredSessions.value.orEmpty()
+ .filter { session -> session.dayNumber == args.day }
+ .indexOfFirst { session -> session.isOnGoing }
binding.sessionsRecycler.scrollToPosition(position)
}

diff:
@@ -107,7 +107,7 @@ class SessionPagesStore @Inject constructor(
}
val filtersValue: Filters get() = filters.requireValue()
- private val filteredSessions: LiveData<List<Session>> = sessions
+ val filteredSessions: LiveData<List<Session>> = sessions
.combineWith(filters) { sessions, filters ->
sessions ?: return@combineWith listOf<Session>()
sessions.filter { session ->
@@ -146,8 +146,4 @@ class SessionPagesStore @Inject constructor(
.filter { session -> session.isFavorited }
}
}
-
- fun onGoingSessionIndex(day: Int) = filteredSessions.value.orEmpty()
- .filter { session -> session.dayNumber == day }
- .indexOfFirst { session -> session.isOnGoing }
}

original_message: Move code of getting index to Fragment


message: Add favorite button

diff:
@@ -27,6 +27,9 @@ final class SessionDataSource: NSObject, UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: SessionTableViewCell = tableView.dequeueReusableCell(forIndexPath: indexPath)
cell.session = items[indexPath.section].sessions[indexPath.row]
+ cell.favoriteButton.rx.tap
+ .subscribe { _ in cell.favoriteButton.isSelected = !cell.favoriteButton.isSelected }
+ .disposed(by: cell.bag)
return cell
}
}

original_message: Add favorite button


message: Create a stream to observe favorite button tap events

diff:
@@ -16,6 +16,8 @@ final class SessionDataSource: NSObject, UITableViewDataSource {
typealias Element = [SessionByStartTime]
var items: Element = []
+ var toggleFavorite = PublishSubject<String>()
+
func numberOfSections(in tableView: UITableView) -> Int {
return items.count
}
@@ -26,9 +28,11 @@ final class SessionDataSource: NSObject, UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: SessionTableViewCell = tableView.dequeueReusableCell(forIndexPath: indexPath)
- cell.session = items[indexPath.section].sessions[indexPath.row]
- cell.favoriteButton.rx.tap
- .subscribe { _ in cell.favoriteButton.isSelected = !cell.favoriteButton.isSelected }
+ let session = items[indexPath.section].sessions[indexPath.row]
+ cell.session = session
+ cell.favoriteButtonDidTapped
+ .map { _ in session.id_ }
+ .bind(to: toggleFavorite)
.disposed(by: cell.bag)
return cell
}

diff:
@@ -57,6 +57,10 @@ final class SessionsViewController: UIViewController, StoryboardInstantiable {
output.sessions
.drive(tableView.rx.items(dataSource: dataSource))
.disposed(by: bag)
+
+ dataSource.toggleFavorite
+ .subscribe { print($0) }
+ .disposed(by: bag)
}
}


message: Add favorite repository to store session ids

diff:
3672C2DD21F7392900ACB910 /* Kingfisher.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3672C2DC21F7392900ACB910 /* Kingfisher.framework */; };
3672C2DF21F73A9400ACB910 /* SpeakerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3672C2DE21F73A9400ACB910 /* SpeakerCell.swift */; };
36D1FA2721F5CF6600ADBBDE /* StoryboardInstantiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36D1FA2621F5CF6600ADBBDE /* StoryboardInstantiable.swift */; };
+ 43DC80C12200A0F600A3F710 /* FavoriteRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43DC80C02200A0F600A3F710 /* FavoriteRepository.swift */; };
5917BCA621FA19B8005BEAF2 /* SessionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5917BCA521FA19B8005BEAF2 /* SessionHeaderView.swift */; };
59D30FC921F8B1BD003A937C /* SessionByStartTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59D30FC821F8B1BD003A937C /* SessionByStartTime.swift */; };
63F34E1B21EB43CE00B2EC48 /* SessionRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F34E1A21EB43CE00B2EC48 /* SessionRepository.swift */; };
3672C2DC21F7392900ACB910 /* Kingfisher.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Kingfisher.framework; path = Carthage/Build/iOS/Kingfisher.framework; sourceTree = "<group>"; };
3672C2DE21F73A9400ACB910 /* SpeakerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeakerCell.swift; sourceTree = "<group>"; };
36D1FA2621F5CF6600ADBBDE /* StoryboardInstantiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryboardInstantiable.swift; sourceTree = "<group>"; };
+ 43DC80C02200A0F600A3F710 /* FavoriteRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteRepository.swift; sourceTree = "<group>"; };
5917BCA521FA19B8005BEAF2 /* SessionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionHeaderView.swift; sourceTree = "<group>"; };
59D30FC821F8B1BD003A937C /* SessionByStartTime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionByStartTime.swift; sourceTree = "<group>"; };
63F34E1A21EB43CE00B2EC48 /* SessionRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionRepository.swift; sourceTree = "<group>"; };
isa = PBXGroup;
children = (
2159CC5621FE186800DBD016 /* Day.swift */,
- 63F34E1A21EB43CE00B2EC48 /* SessionRepository.swift */,
+ 43DC80C02200A0F600A3F710 /* FavoriteRepository.swift */,
59D30FC821F8B1BD003A937C /* SessionByStartTime.swift */,
+ 63F34E1A21EB43CE00B2EC48 /* SessionRepository.swift */,
363E6AF321FD536A00EBC698 /* TagContent.swift */,
);
path = data;
366B060721F751C400C912AD /* TagsLeftAlignedCollectionViewFlowLayout.swift in Sources */,
3672C2D121F6033200ACB910 /* NibLoadable.swift in Sources */,
3672C2CD21F6024B00ACB910 /* Reusable.swift in Sources */,
+ 43DC80C12200A0F600A3F710 /* FavoriteRepository.swift in Sources */,
366B060E21F7652D00C912AD /* PaddingLabel.swift in Sources */,
5917BCA621FA19B8005BEAF2 /* SessionHeaderView.swift in Sources */,
36D1FA2721F5CF6600ADBBDE /* StoryboardInstantiable.swift in Sources */,

diff:
@@ -12,7 +12,8 @@ import RxCocoa
final class SessionsViewModel {
- private let repository = SessionRepository()
+ private let sessionRepository = SessionRepository()
+ private let favoriteRepository = FavoriteRepository()
private let bag = DisposeBag()
private let day: Day
init(day: Day) {
@@ -36,7 +37,7 @@ extension SessionsViewModel {
let sessionContents = input.viewWillAppear
.flatMap { [weak self] (_) -> Observable<SessionContents> in
guard let `self` = self else { return Observable.empty() }
- return self.repository.fetch()
+ return self.sessionRepository.fetch()
.asObservable()
.catchError { error in
self._error.accept(error.localizedDescription)
@@ -44,6 +45,10 @@ extension SessionsViewModel {
}
}
+ let favoriteSessionIds = favoriteRepository.sessionIdsDidChanged
+
+ favoriteSessionIds.subscribe { print($0) }.disposed(by: bag)
+
let dateFormatter = DateFormatter()
dateFormatter.timeZone = TimeZone(identifier: "Asia/Tokyo")
dateFormatter.locale = NSLocale.current

diff:
+//
+// FavoriteRepository.swift
+// DroidKaigi 2019
+//
+// Created by woxtu on 2019/01/29.
+//
+
+import Foundation
+import RxSwift
+
+final class FavoriteRepository {
+ private let key = "favoriteSessionIds"
+
+ var sessionIdsDidChanged: Observable<[String]> {
+ return UserDefaults.standard.rx
+ .observe([String].self, key, options: [.initial, .new])
+ .map { $0 ?? [] }
+ }
+
+ func toggle(sessionId: String) {
+ var favorites = UserDefaults.standard.object(forKey: key) as? [String] ?? []
+ if favorites.contains(sessionId) {
+ favorites.removeAll { $0 == sessionId }
+ } else {
+ favorites.append(sessionId)
+ }
+ UserDefaults.standard.setValue(favorites, forKey: key)
+ UserDefaults.standard.synchronize()
+ }
+}


message: Combine session stream with favorite stream

diff:
import Foundation
import ioscombined
+extension Session {
+
+ var startTimeText: String {
+ struct Static {
+ static let dateFormatter: DateFormatter = {
+ let dateFormatter = DateFormatter()
+ dateFormatter.timeZone = TimeZone(identifier: "Asia/Tokyo")
+ dateFormatter.locale = NSLocale.current
+ dateFormatter.dateFormat = "HH:mm"
+ return dateFormatter
+ }()
+ }
+ return Static.dateFormatter.string(from: Date(timeIntervalSince1970: startTime / 1000))
+ }
+}
+
extension SpeechSession {
var tagContents: [TagContent] {
@@ -22,4 +38,69 @@ extension SpeechSession {
tags.append(.category(category: category))
return tags
}
+
+ // redefine for now
+ func doCopy(id: String? = nil,
+ dayNumber: Int32? = nil,
+ startTime: Double? = nil,
+ endTime: Double? = nil,
+ title: LocaledString? = nil,
+ desc: String? = nil,
+ room: Room? = nil,
+ format: String? = nil,
+ lang: Lang? = nil,
+ category: ioscombined.Category? = nil,
+ intendedAudience: String? = nil,
+ videoUrl: String? = nil,
+ slideUrl: String? = nil,
+ isInterpretationTarget: Bool? = nil,
+ isFavorited: Bool? = nil,
+ speakers: [Speaker]? = nil,
+ forBeginners: Bool? = nil,
+ message: LocaledString? = nil
+ ) -> SpeechSession {
+ return doCopy(id: id ?? self.id_,
+ dayNumber: dayNumber ?? self.dayNumber,
+ startTime: startTime ?? self.startTime,
+ endTime: endTime ?? self.endTime,
+ title: title ?? self.title,
+ desc: desc ?? self.desc,
+ room: room ?? self.room,
+ format: format ?? self.format,
+ lang: lang ?? self.lang,
+ category: category ?? self.category,
+ intendedAudience: intendedAudience ?? self.intendedAudience,
+ videoUrl: videoUrl ?? self.videoUrl,
+ slideUrl: slideUrl ?? self.slideUrl,
+ isInterpretationTarget: isInterpretationTarget ?? self.isInterpretationTarget,
+ isFavorited: isFavorited ?? self.isFavorited,
+ speakers: speakers ?? self.speakers,
+ forBeginners: forBeginners ?? self.forBeginners,
+ message: message ?? self.message)
+ }
+}
+
+extension ServiceSession {
+
+ // redefine for now
+ func doCopy(id: String? = nil,
+ dayNumber: Int32? = nil,
+ startTime: Double? = nil,
+ endTime: Double? = nil,
+ title: LocaledString? = nil,
+ desc: String? = nil,
+ room: Room? = nil,
+ sessionType: SessionType? = nil,
+ isFavorited: Bool? = nil
+ ) -> ServiceSession {
+ return doCopy(id: id ?? self.id_,
+ dayNumber: dayNumber ?? self.dayNumber,
+ startTime: startTime ?? self.startTime,
+ endTime: endTime ?? self.endTime,
+ title: title ?? self.title,
+ desc: desc ?? self.desc,
+ room: room ?? self.room,
+ sessionType: sessionType ?? self.sessionType,
+ isFavorited: isFavorited ?? self.isFavorited)
+ }
}

diff:
@@ -16,7 +16,7 @@ final class SessionDataSource: NSObject, UITableViewDataSource {
typealias Element = [SessionByStartTime]
var items: Element = []
- var toggleFavorite = PublishSubject<String>()
+ var toggleFavorite = PublishSubject<Session>()
func numberOfSections(in tableView: UITableView) -> Int {
return items.count
@@ -31,7 +31,7 @@ final class SessionDataSource: NSObject, UITableViewDataSource {
let session = items[indexPath.section].sessions[indexPath.row]
cell.session = session
cell.favoriteButtonDidTapped
- .map { _ in session.id_ }
+ .map { _ in session }
.bind(to: toggleFavorite)
.disposed(by: cell.bag)
return cell

diff:
@@ -45,7 +45,8 @@ final class SessionsViewController: UIViewController, StoryboardInstantiable {
private func bind() {
let viewWillAppear = rx.methodInvoked(#selector(self.viewWillAppear)).map { _ in }
- let input = SessionsViewModel.Input(viewWillAppear: viewWillAppear)
+ let toggleFavorite = dataSource.toggleFavorite
+ let input = SessionsViewModel.Input(viewWillAppear: viewWillAppear, toggleFavorite: toggleFavorite)
let output = viewModel.transform(input: input)
output.error
.drive(onNext: { errorMessage in
@@ -57,10 +58,6 @@ final class SessionsViewController: UIViewController, StoryboardInstantiable {
output.sessions
.drive(tableView.rx.items(dataSource: dataSource))
.disposed(by: bag)
-
- dataSource.toggleFavorite
- .subscribe { print($0) }
- .disposed(by: bag)
}
}

diff:
@@ -26,6 +26,7 @@ extension SessionsViewModel {
struct Input {
let viewWillAppear: Observable<Void>
+ let toggleFavorite: Observable<Session>
}
struct Output {
@@ -34,6 +35,10 @@ extension SessionsViewModel {
}
func transform(input: Input) -> Output {
+ input.toggleFavorite
+ .subscribe(onNext: { [weak self] in self?.favoriteRepository.toggle(sessionId: $0.id_) })
+ .disposed(by: bag)
+
let sessionContents = input.viewWillAppear
.flatMap { [weak self] (_) -> Observable<SessionContents> in
guard let `self` = self else { return Observable.empty() }
@@ -47,28 +52,32 @@ extension SessionsViewModel {
let favoriteSessionIds = favoriteRepository.sessionIdsDidChanged
- favoriteSessionIds.subscribe { print($0) }.disposed(by: bag)
-
- let dateFormatter = DateFormatter()
- dateFormatter.timeZone = TimeZone(identifier: "Asia/Tokyo")
- dateFormatter.locale = NSLocale.current
- dateFormatter.dateFormat = "HH:mm"
-
- let sessions = sessionContents.map { [weak self] sessionContents -> [SessionByStartTime] in
+ let sessions = Observable.combineLatest(sessionContents, favoriteSessionIds)
+ .map { [weak self] (sessionContents, favoriteSessionIds) -> [SessionByStartTime] in
guard let `self` = self else { return [] }
return sessionContents.sessions
.filter { $0.dayNumber == self.day.day }
+ .map { (session: Session) -> Session in
+ if let session = session as? ServiceSession {
+ return session.doCopy(isFavorited: favoriteSessionIds.contains(session.id_))
+ } else if let session = session as? SpeechSession {
+ return session.doCopy(isFavorited: favoriteSessionIds.contains(session.id_))
+ } else {
+ return session
+ }
+ }
.reduce(into: [SessionByStartTime]()) { result, session in
if let index = result.firstIndex(where: { $0.startTime == session.startTime }) {
result[index].sessions.append(session)
return
}
result.append(SessionByStartTime(startDayText: session.startDayText,
- startTimeText: dateFormatter.string(from: Date(timeIntervalSince1970: session.startTime / 1000)),
+ startTimeText: session.startTimeText,
startTime: session.startTime,
sessions: [session]))
}
- }.asDriver(onErrorJustReturn: [])
+ }
+ .asDriver(onErrorJustReturn: [])
let error = _error.asDriver()
return Output(sessions: sessions, error: error)
}


message: Fix jumpy scrolling caused by reloading data

diff:
@@ -18,6 +18,8 @@ final class SessionDataSource: NSObject, UITableViewDataSource {
var toggleFavorite = PublishSubject<Session>()
+ private var cellHeightsCache = [IndexPath: CGFloat]()
+
func numberOfSections(in tableView: UITableView) -> Int {
return items.count
}
@@ -39,6 +41,7 @@ final class SessionDataSource: NSObject, UITableViewDataSource {
}
extension SessionDataSource: UITableViewDelegate {
+
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let view = SessionHeaderView(frame: CGRect(x: 0,
y: 0,
@@ -48,9 +51,17 @@ extension SessionDataSource: UITableViewDelegate {
return view
}
- public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: false)
}
+
+ func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
+ cellHeightsCache[indexPath] = cell.frame.height
+ }
+
+ func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
+ return cellHeightsCache[indexPath] ?? UITableView.automaticDimension
+ }
}
extension SessionDataSource: RxTableViewDataSourceType {
@@ -65,6 +76,7 @@ extension SessionDataSource: RxTableViewDataSourceType {
}
extension SessionDataSource: SectionedViewDataSourceType {
+
func model(at indexPath: IndexPath) throws -> Any {
return items[indexPath.section].sessions[indexPath.row]
}


message: Set button images

diff:
3672C2DF21F73A9400ACB910 /* SpeakerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3672C2DE21F73A9400ACB910 /* SpeakerCell.swift */; };
36D1FA2721F5CF6600ADBBDE /* StoryboardInstantiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36D1FA2621F5CF6600ADBBDE /* StoryboardInstantiable.swift */; };
43DC80C12200A0F600A3F710 /* FavoriteRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43DC80C02200A0F600A3F710 /* FavoriteRepository.swift */; };
+ 43DC80C72201FC8100A3F710 /* UIButton+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43DC80C62201FC8100A3F710 /* UIButton+Extension.swift */; };
5917BCA621FA19B8005BEAF2 /* SessionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5917BCA521FA19B8005BEAF2 /* SessionHeaderView.swift */; };
59D30FC921F8B1BD003A937C /* SessionByStartTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59D30FC821F8B1BD003A937C /* SessionByStartTime.swift */; };
63F34E1B21EB43CE00B2EC48 /* SessionRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F34E1A21EB43CE00B2EC48 /* SessionRepository.swift */; };
3672C2DE21F73A9400ACB910 /* SpeakerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeakerCell.swift; sourceTree = "<group>"; };
36D1FA2621F5CF6600ADBBDE /* StoryboardInstantiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryboardInstantiable.swift; sourceTree = "<group>"; };
43DC80C02200A0F600A3F710 /* FavoriteRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteRepository.swift; sourceTree = "<group>"; };
+ 43DC80C62201FC8100A3F710 /* UIButton+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+Extension.swift"; sourceTree = "<group>"; };
5917BCA521FA19B8005BEAF2 /* SessionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionHeaderView.swift; sourceTree = "<group>"; };
59D30FC821F8B1BD003A937C /* SessionByStartTime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionByStartTime.swift; sourceTree = "<group>"; };
63F34E1A21EB43CE00B2EC48 /* SessionRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionRepository.swift; sourceTree = "<group>"; };
3672C2CA21F601D600ACB910 /* UITableView+Extension.swift */,
366B060A21F7604400C912AD /* UICollectionView+Extension.swift */,
363E6AF521FD545B00EBC698 /* Session+Extension.swift */,
+ 43DC80C62201FC8100A3F710 /* UIButton+Extension.swift */,
);
path = Extensions;
sourceTree = "<group>";
63F34E1B21EB43CE00B2EC48 /* SessionRepository.swift in Sources */,
59D30FC921F8B1BD003A937C /* SessionByStartTime.swift in Sources */,
362546CD21FCD4FF005ADB0F /* SessionDetailSectionFooterView.swift in Sources */,
+ 43DC80C72201FC8100A3F710 /* UIButton+Extension.swift in Sources */,
3672C2D321F6FB7E00ACB910 /* SessionDataSource.swift in Sources */,
362546C921FC9DCD005ADB0F /* SessionDetailWithIconTableViewCell.swift in Sources */,
366B060721F751C400C912AD /* TagsLeftAlignedCollectionViewFlowLayout.swift in Sources */,

diff:
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "ic_bookmark_48pt.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "ic_bookmark_48pt_2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "ic_bookmark_48pt_3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file

diff: Binary files /dev/null and b/frontend/ios/DroidKaigi 2019/Assets.xcassets/bookmark.imageset/ic_bookmark_48pt.png differ

diff: Binary files /dev/null and b/frontend/ios/DroidKaigi 2019/Assets.xcassets/bookmark.imageset/ic_bookmark_48pt_2x.png differ

diff: Binary files /dev/null and b/frontend/ios/DroidKaigi 2019/Assets.xcassets/bookmark.imageset/ic_bookmark_48pt_3x.png differ

diff:
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "ic_bookmark_border_48pt.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "ic_bookmark_border_48pt_2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "ic_bookmark_border_48pt_3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file

diff: Binary files /dev/null and b/frontend/ios/DroidKaigi 2019/Assets.xcassets/bookmark_border.imageset/ic_bookmark_border_48pt.png differ

diff: Binary files /dev/null and b/frontend/ios/DroidKaigi 2019/Assets.xcassets/bookmark_border.imageset/ic_bookmark_border_48pt_2x.png differ

diff: Binary files /dev/null and b/frontend/ios/DroidKaigi 2019/Assets.xcassets/bookmark_border.imageset/ic_bookmark_border_48pt_3x.png differ

diff:
+//
+// UIButton+Extension.swift
+// DroidKaigi 2019
+//
+// Created by woxtu on 2019/01/31.
+//
+
+import UIKit
+
+extension UIButton {
+ func setImageTintColor(color: UIColor, for state: UIControl.State) {
+ if let image = image(for: state) {
+ setImage(applyTintColor(color: color, to: image), for: state)
+ }
+ }
+
+ private func applyTintColor(color: UIColor, to image: UIImage) -> UIImage? {
+ UIGraphicsBeginImageContextWithOptions(image.size, false, UIScreen.main.scale)
+
+ guard let context = UIGraphicsGetCurrentContext() else {
+ return image
+ }
+
+ let rect = CGRect(origin: .zero, size: image.size)
+
+ context.translateBy(x: 0, y: image.size.height)
+ context.scaleBy(x: 1, y: -1)
+
+ if let image = image.cgImage {
+ context.setBlendMode(.normal)
+ context.draw(image, in: rect)
+ }
+
+ context.setBlendMode(.sourceIn)
+ context.setFillColor(color.cgColor)
+ context.fill(rect)
+
+ let image = UIGraphicsGetImageFromCurrentImageContext()
+ UIGraphicsEndImageContext()
+ return image
+ }
+}

original_message: Set button images


message: Introduce stack view for title

diff:
@@ -99,6 +99,15 @@ class SessionTableViewCell: UITableViewCell, Reusable {
return MDCInkTouchController(view: self)
}()
+ private lazy var titleStackView: UIStackView = {
+ let stackView = UIStackView()
+ stackView.axis = .horizontal
+ stackView.alignment = .center
+ stackView.distribution = .fill
+ stackView.spacing = 8
+ stackView.backgroundColor = .blue
+ return stackView
+ }()
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 16, weight: .bold)
@@ -154,31 +163,29 @@ class SessionTableViewCell: UITableViewCell, Reusable {
}()
private func setupSubviews() {
- [titleLabel, liveMark, favoriteButton, speakersStackView, timeAndRoomLabel, collectionView].forEach(contentView.addSubview)
- titleLabel.snp.makeConstraints {
+ [titleLabel, liveMark, favoriteButton].forEach(titleStackView.addArrangedSubview)
+
+ [titleStackView, speakersStackView, timeAndRoomLabel, collectionView].forEach(contentView.addSubview)
+ titleStackView.snp.makeConstraints {
$0.top.equalToSuperview().inset(5)
$0.leading.equalToSuperview().inset(90)
- $0.trailing.equalTo(liveMark.snp.leading).offset(-4)
+ $0.trailing.equalToSuperview().inset(16)
}
liveMark.snp.makeConstraints {
- $0.top.equalTo(titleLabel)
- $0.trailing.equalTo(favoriteButton.snp.leading)
$0.width.equalTo(32)
$0.height.equalTo(16)
}
favoriteButton.snp.makeConstraints {
- $0.centerY.equalTo(titleLabel.snp.centerY)
- $0.trailing.equalToSuperview().inset(16)
$0.width.height.equalTo(24)
}
speakersStackView.snp.makeConstraints {
- $0.top.equalTo(titleLabel.snp.bottom).offset(8)
- $0.leading.equalTo(titleLabel)
+ $0.top.equalTo(titleStackView.snp.bottom).offset(8)
+ $0.leading.equalTo(titleStackView)
$0.trailing.equalToSuperview().inset(16)
}
remakeTimeAndRoomLabelConstraints()
collectionView.snp.makeConstraints {
- $0.leading.equalTo(titleLabel)
+ $0.leading.equalTo(titleStackView)
$0.trailing.equalToSuperview().inset(16)
$0.top.equalTo(timeAndRoomLabel.snp.bottom).offset(7)
$0.bottom.equalToSuperview().inset(26)

original_message: Introduce stack view for title


message: Scroll to the current session when a Fragment is loaded

diff:
@@ -131,6 +131,9 @@ class BottomSheetDaySessionsFragment : DaggerFragment() {
?.startDayText ?: return@changed
binding.sessionsBottomSheetTitle.text = titleText
}
+ sessionPagesStore.firstLoadSessions.changed(viewLifecycleOwner) {
+ scrollToCurrentSession()
+ }
sessionPagesStore.filters.changed(viewLifecycleOwner) {
binding.isFiltered = it.isFiltered()
}

diff:
@@ -5,7 +5,9 @@ import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.ViewModel
import com.shopify.livedataktx.combineWith
import com.shopify.livedataktx.filter
+import com.shopify.livedataktx.first
import com.shopify.livedataktx.map
+import com.shopify.livedataktx.nonNull
import io.github.droidkaigi.confsched2019.action.Action
import io.github.droidkaigi.confsched2019.dispatcher.Dispatcher
import io.github.droidkaigi.confsched2019.ext.android.requireValue
@@ -125,6 +127,11 @@ class SessionPagesStore @Inject constructor(
.map { it.sessionPage }
.toSingleLiveData(SessionPage.pages[0])
+ val firstLoadSessions = filteredSessions
+ .nonNull()
+ .filter { it.isNotEmpty() }
+ .first()
+
fun filteredSessionsByDay(day: Int): LiveData<List<Session>> {
return filteredSessions
.map { sessions ->


message: Add observer when Fragment is created first only

diff:
@@ -131,9 +131,6 @@ class BottomSheetDaySessionsFragment : DaggerFragment() {
?.startDayText ?: return@changed
binding.sessionsBottomSheetTitle.text = titleText
}
- sessionPagesStore.firstLoadSessions.changed(viewLifecycleOwner) {
- scrollToCurrentSession()
- }
sessionPagesStore.filters.changed(viewLifecycleOwner) {
binding.isFiltered = it.isFiltered()
}
@@ -155,6 +152,12 @@ class BottomSheetDaySessionsFragment : DaggerFragment() {
scrollToCurrentSession()
}
}
+
+ if (savedInstanceState == null) {
+ sessionPagesStore.firstLoadSessions.observe(viewLifecycleOwner) {
+ scrollToCurrentSession()
+ }
+ }
}
override fun onDestroyView() {

diff:
@@ -127,9 +127,10 @@ class SessionPagesStore @Inject constructor(
.map { it.sessionPage }
.toSingleLiveData(SessionPage.pages[0])
- val firstLoadSessions = filteredSessions
+ val firstLoadSessions: LiveData<Unit> = filteredSessions
.nonNull()
.filter { it.isNotEmpty() }
+ .map { Unit }
.first()
fun filteredSessionsByDay(day: Int): LiveData<List<Session>> {


message: Do not use a bundle and use a isFirstLoadScrolled flag

diff:
@@ -153,9 +153,10 @@ class BottomSheetDaySessionsFragment : DaggerFragment() {
}
}
- if (savedInstanceState == null) {
- sessionPagesStore.firstLoadSessions.observe(viewLifecycleOwner) {
+ sessionPagesStore.firstLoadSessions.changed(viewLifecycleOwner) {
+ if (!sessionPagesStore.isFirstLoadScrolled) {
scrollToCurrentSession()
+ sessionPagesStore.isFirstLoadScrolled = true
}
}
}

diff:
@@ -22,6 +22,8 @@ import javax.inject.Inject
class SessionPagesStore @Inject constructor(
dispatcher: Dispatcher
) : ViewModel() {
+ var isFirstLoadScrolled: Boolean = false
+
private val sessions: LiveData<List<Session>> = dispatcher
.subscribe<Action.SessionsLoaded>()
.map { it.sessions }


message: Observe the top visible session

diff:
@@ -16,7 +16,8 @@ final class SessionDataSource: NSObject, UITableViewDataSource {
typealias Element = [SessionByStartTime]
var items: Element = []
- var toggleFavorite = PublishSubject<Session>()
+ let topVisibleSession = PublishSubject<Session>()
+ let toggleFavorite = PublishSubject<Session>()
private var cellHeightsCache = [IndexPath: CGFloat]()
@@ -64,12 +65,29 @@ extension SessionDataSource: UITableViewDelegate {
}
}
+extension SessionDataSource: UIScrollViewDelegate {
+
+ func scrollViewDidScroll(_ scrollView: UIScrollView) {
+ if let tableView = scrollView as? UITableView,
+ let indexPath = tableView.indexPathsForVisibleRows?.first {
+ let session = items[indexPath.section].sessions[indexPath.row]
+ topVisibleSession.onNext(session)
+ }
+ }
+}
+
extension SessionDataSource: RxTableViewDataSourceType {
- public func tableView(_ tableView: UITableView, observedEvent: Event<Element>) {
+ func tableView(_ tableView: UITableView, observedEvent: Event<Element>) {
Binder(self) { dataSource, element in
dataSource.items = element
tableView.reloadData()
+
+ // get the top visible session before scrolling table view
+ if let indexPath = tableView.indexPathsForVisibleRows?.first {
+ let session = dataSource.items[indexPath.section].sessions[indexPath.row]
+ dataSource.topVisibleSession.onNext(session)
+ }
}
.on(observedEvent)
}

diff:
@@ -67,6 +67,10 @@ final class SessionsViewController: UIViewController {
output.sessions
.drive(tableView.rx.items(dataSource: dataSource))
.disposed(by: bag)
+
+ dataSource.topVisibleSession
+ .subscribe(onNext: { print($0.id_) })
+ .disposed(by: bag)
}
}

original_message: Observe the top visible session


message: Add a fixed table header view

diff:
3672C2DD21F7392900ACB910 /* Kingfisher.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3672C2DC21F7392900ACB910 /* Kingfisher.framework */; };
3672C2DF21F73A9400ACB910 /* SpeakerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3672C2DE21F73A9400ACB910 /* SpeakerCell.swift */; };
36D1FA2721F5CF6600ADBBDE /* StoryboardInstantiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36D1FA2621F5CF6600ADBBDE /* StoryboardInstantiable.swift */; };
+ 439CE2B22206356700AC597A /* SessionTableHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439CE2B12206356700AC597A /* SessionTableHeaderView.swift */; };
43DC80C12200A0F600A3F710 /* FavoriteRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43DC80C02200A0F600A3F710 /* FavoriteRepository.swift */; };
43DC80C72201FC8100A3F710 /* UIButton+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43DC80C62201FC8100A3F710 /* UIButton+Extension.swift */; };
5917BCA621FA19B8005BEAF2 /* SessionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5917BCA521FA19B8005BEAF2 /* SessionHeaderView.swift */; };
3672C2DC21F7392900ACB910 /* Kingfisher.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Kingfisher.framework; path = Carthage/Build/iOS/Kingfisher.framework; sourceTree = "<group>"; };
3672C2DE21F73A9400ACB910 /* SpeakerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeakerCell.swift; sourceTree = "<group>"; };
36D1FA2621F5CF6600ADBBDE /* StoryboardInstantiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryboardInstantiable.swift; sourceTree = "<group>"; };
+ 439CE2B12206356700AC597A /* SessionTableHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionTableHeaderView.swift; sourceTree = "<group>"; };
43DC80C02200A0F600A3F710 /* FavoriteRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteRepository.swift; sourceTree = "<group>"; };
43DC80C62201FC8100A3F710 /* UIButton+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+Extension.swift"; sourceTree = "<group>"; };
5917BCA521FA19B8005BEAF2 /* SessionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionHeaderView.swift; sourceTree = "<group>"; };
366B060621F751C400C912AD /* TagsLeftAlignedCollectionViewFlowLayout.swift */,
366B060821F756CB00C912AD /* TagsCollectionViewCell.swift */,
5917BCA521FA19B8005BEAF2 /* SessionHeaderView.swift */,
+ 439CE2B12206356700AC597A /* SessionTableHeaderView.swift */,
);
path = Session;
sourceTree = "<group>";
362D90FD21F5DBC2006962C8 /* StartViewController.swift in Sources */,
362546CB21FCD4E8005ADB0F /* SessionDetailSectionHeaderView.swift in Sources */,
360744F321FAF0CE003569D3 /* SessionDetailHeaderTableViewCell.swift in Sources */,
+ 439CE2B22206356700AC597A /* SessionTableHeaderView.swift in Sources */,
360744F521FAF13C003569D3 /* SessionDetailTableViewCell.swift in Sources */,
362D910421F5DC2D006962C8 /* SessionsViewModel.swift in Sources */,
360744F121FADF53003569D3 /* SessionDetailViewController.swift in Sources */,

diff:
+//
+// SessionTableHeaderView.swift
+// DroidKaigi 2019
+//
+// Created by woxtu on 2019/02/03.
+//
+
+import UIKit
+import RxCocoa
+import RxSwift
+import SnapKit
+
+class SessionTableHeaderView: UIView {
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+ commonInit()
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ commonInit()
+ }
+
+ private func commonInit() {
+ setupSubviews()
+ }
+
+ private lazy var startDayLabel: UILabel = {
+ let label = UILabel()
+ label.numberOfLines = 1
+ label.font = UIFont.systemFont(ofSize: 20)
+ label.textColor = UIColor(hex: "95959c")
+ label.text = "2019.2.7"
+ return label
+ }()
+
+ private func setupSubviews() {
+ addSubview(startDayLabel)
+ startDayLabel.snp.makeConstraints {
+ $0.centerY.equalToSuperview()
+ $0.leading.equalToSuperview().inset(20)
+ }
+ }
+}

diff:
@@ -18,9 +18,14 @@ final class SessionsViewController: UIViewController {
init(viewModel: SessionsViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
- view.addSubview(tableView)
+ [tableHeaderView, tableView].forEach(view.addSubview)
+ tableHeaderView.snp.makeConstraints {
+ $0.top.leading.trailing.equalToSuperview()
+ $0.height.equalTo(48)
+ }
tableView.snp.makeConstraints {
- $0.edges.equalToSuperview()
+ $0.top.equalTo(tableHeaderView.snp.bottom)
+ $0.leading.trailing.bottom.equalToSuperview()
}
}
required init?(coder aDecoder: NSCoder) { fatalError() }
@@ -41,6 +46,8 @@ final class SessionsViewController: UIViewController {
private let dataSource = SessionDataSource()
+ private lazy var tableHeaderView = SessionTableHeaderView()
+
private lazy var tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .plain)
tableView.backgroundColor = .white

original_message: Add a fixed table header view


message: Show the start day on the table header view

diff:
@@ -12,6 +12,12 @@ import SnapKit
class SessionTableHeaderView: UIView {
+ var startDayText: Binder<String> {
+ return Binder(startDayLabel) { label, text in
+ label.text = text
+ }
+ }
+
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
@@ -31,7 +37,6 @@ class SessionTableHeaderView: UIView {
label.numberOfLines = 1
label.font = UIFont.systemFont(ofSize: 20)
label.textColor = UIColor(hex: "95959c")
- label.text = "2019.2.7"
return label
}()

diff:
@@ -61,8 +61,11 @@ final class SessionsViewController: UIViewController {
private func bind() {
let viewWillAppear = rx.methodInvoked(#selector(self.viewWillAppear)).map { _ in }
+ let topVisibleSession = dataSource.topVisibleSession
let toggleFavorite = dataSource.toggleFavorite
- let input = SessionsViewModel.Input(viewWillAppear: viewWillAppear, toggleFavorite: toggleFavorite)
+ let input = SessionsViewModel.Input(viewWillAppear: viewWillAppear,
+ topVisibleSession: topVisibleSession,
+ toggleFavorite: toggleFavorite)
let output = viewModel.transform(input: input)
output.error
.drive(onNext: { errorMessage in
@@ -71,12 +74,13 @@ final class SessionsViewController: UIViewController {
}
})
.disposed(by: bag)
- output.sessions
- .drive(tableView.rx.items(dataSource: dataSource))
+
+ output.startDayText
+ .drive(tableHeaderView.startDayText)
.disposed(by: bag)
- dataSource.topVisibleSession
- .subscribe(onNext: { print($0.id_) })
+ output.sessions
+ .drive(tableView.rx.items(dataSource: dataSource))
.disposed(by: bag)
}
}

diff:
@@ -40,10 +40,12 @@ extension SessionsViewModel {
struct Input {
let viewWillAppear: Observable<Void>
+ let topVisibleSession: Observable<Session>
let toggleFavorite: Observable<Session>
}
struct Output {
+ let startDayText: Driver<String>
let sessions: Driver<[SessionByStartTime]>
let error: Driver<String?>
}
@@ -53,6 +55,11 @@ extension SessionsViewModel {
.subscribe(onNext: { [weak self] in self?.favoriteRepository.toggle(sessionId: $0.id_) })
.disposed(by: bag)
+ let startDayText = input.topVisibleSession
+ .map { $0.startDayText }
+ .distinctUntilChanged()
+ .asDriver(onErrorJustReturn: "")
+
let sessionContents = input.viewWillAppear
.flatMap { [weak self] (_) -> Observable<SessionContents> in
guard let `self` = self else { return Observable.empty() }
@@ -99,6 +106,6 @@ extension SessionsViewModel {
}
.asDriver(onErrorJustReturn: [])
let error = _error.asDriver()
- return Output(sessions: sessions, error: error)
+ return Output(startDayText: startDayText, sessions: sessions, error: error)
}
}


message: Fix current scroll logic

diff:
@@ -187,9 +187,13 @@ class BottomSheetDaySessionsFragment : DaggerFragment() {
}
private fun scrollToCurrentSession() {
+ val targetSession = sessionPagesStore.filteredSessions.value.orEmpty()
+ .filter { session -> session.isOnGoing }
+ .distinctBy { session -> session.startTime }
+ .lastOrNull()
val position = sessionPagesStore.filteredSessions.value.orEmpty()
.filter { session -> session.dayNumber == args.day }
- .indexOfFirst { session -> session.isOnGoing }
+ .indexOf(targetSession)
binding.sessionsRecycler.scrollToPosition(position)
}

original_message: Fix current scroll logic


message:
Update and rename ROADMAP.md to docs/ROADMAP.md
Added Bashate to Testing.

diff:
@@ -45,8 +45,9 @@ _(this is the active to-do list, not specifically in any order)_
- - [ ] Contribution guidelines
- - [ ] License
- [ ] Testing
-- - [ ] Travis
-- - [ ] Shellcheck
+- - - [ ] Travis
+- - - [ ] Shellcheck
+- - - [ ] Bashate
### 1.1

repo: ghostwriters/dockstarter


message:
Update README.md - Added Gitter, Travis, Feathub
Added Gitter, Travis, Feathub

diff:
# DockSTARTer
-### [Roadmap](ROADMAP.md)
+[![Chat Gitter](https://badges.gitter.im/DockSTARTer/Lobby.svg)](https://gitter.im/DockSTARTer/Lobby)
+[![Build Status](https://travis-ci.org/GhostWriters/DockSTARTer.svg?branch=master)](https://travis-ci.org/GhostWriters/DockSTARTer)
+
+### [Roadmap](docs/ROADMAP.md)
Before we even get to a proper readme I'd like to start giving credits to those that made this project possible:
@@ -8,3 +11,8 @@ Before we even get to a proper readme I'd like to start giving credits to those
- [SmartHomeBeginner.com](https://www.smarthomebeginner.com/) for creating [AtoMiC-ToolKit](https://github.com/htpcBeginner/AtoMiC-ToolKit) that served as this project's primary inspiration, and later [this](https://www.smarthomebeginner.com/docker-home-media-server-2018-basic/) guide that provided some initial direction with Docker.
- [LinuxServer.io](https://www.linuxserver.io/) for maintaining the majority of the Docker images used in this project.
+
+## Feature Requests
+Please add new features and vote to help steer the direction of DockSTARTer. Current requests and voting can be seen below.
+
+[![Feature Requests](http://feathub.com/GhostWriters/DockSTARTer?format=svg)](http://feathub.com/GhostWriters/DockSTARTer)


message:
Update mypc.conf.example
Added all currently supported apps

diff:
-hydra2
+#couchpotato
+#deluge
+#duckdns
+#duplicati
+#emby
+#headphones
+#hydra2
#jackett
-letsencrypt
-monitorr
-nzbget
-ombi
-organizr
+#lazylibrarian
+#letsencrypt
+#lidarr
+#monitorr
+#muximux
+#nzbget
+#ombi
+#organizr
#plex
+#plexrequests
portainer
-radarr
-sonarr
-tautulli
-transmission
+#radarr
+#rutorrent
+#sabnzbd
+#sickrage
+#sonarr
+#syncthing
+#tautulli
+#transmission
+#unifi
watchtower


message:
Update duckdns.yml
Fixed typo

diff:
@@ -6,9 +6,9 @@ services:
hostname: duckdns
restart: always
environment:
- - DUCKDNS_LOG_FILE=${DUCKDNS_LOG_FILE}
- - DUCKDNS_SUBDOMAINS=${DUCKDNS_SUBDOMAINS}
- - DUCKDNS_TOKEN=${DUCKDNS_TOKEN}
+ - LOG_FILE=${DUCKDNS_LOG_FILE}
+ - SUBDOMAINS=${DUCKDNS_SUBDOMAINS}
+ - TOKEN=${DUCKDNS_TOKEN}
- PGID=${PGID}
- PUID=${PUID}
- TZ=${TZ}

original_message:
Update duckdns.yml

Fixed typo


message: Added home assistant

diff:
+services:
+ headphones:
+ image: homeassistant/aarch64-homeassistant

diff:
+services:
+ headphones:
+ image: homeassistant/raspberrypi3-homeassistant

diff:
+services:
+ homeassistant:
+ ports:
+ - "${HOMEASSISTANT_PORT_8123}:8123"

diff:
+services:
+ homeassistant:
+ network_mode: host

diff:
+version: "3.6"
+services:
+ homeassistant:
+ image: homeassistant/home-assistant
+ container_name: home-assistant
+ restart: always
+ environment:
+ - TZ=${TZ}
+ volumes:
+ - ${DOCKERCONFDIR}/homeassistant:/config

diff:
@@ -51,6 +51,11 @@ HEADPHONES_ENABLED=false
HEADPHONES_NETWORK_MODE=bridge
HEADPHONES_PORT_8181=8181
+### HOME-ASSISTANT
+HOMEASSISTANT_ENABLED=false
+HOMEASSISTANT_NETWORK_MODE=bridge
+HOMEASSISTANT_PORT_8123=8123
+
### HYDRA2
HYDRA2_ENABLED=false
HYDRA2_NETWORK_MODE=bridge

original_message: Added home assistant


message: Added Bazarr

diff:
+services:
+ bazarr:
+ ports:
+ - "${BAZARR_PORT_6767}:6767"

diff:
+version: "3.6"
+services:
+ bazarr:
+ image: morpheus65535/bazarr
+ container_name: bazarr
+ restart: always
+ network_mode: ${BAZARR_NETWORK_MODE}
+ environment:
+ - PGID=${PGID}
+ - PUID=${PUID}
+ - TZ=${TZ}
+ volumes:
+ - ${DOCKERCONFDIR}/bazarr:/bazarr/data
+ - ${MEDIADIR_TV}:/tv

diff:
@@ -22,6 +22,11 @@ COUCHPOTATO_ENABLED=false
COUCHPOTATO_NETWORK_MODE=bridge
COUCHPOTATO_PORT_5050=5050
+### BAZARR
+BAZARR_ENABLED=false
+BAZARR_NETWORK_MODE=bridge
+BAZARR_PORT_6767=6767
+
### DELUGE
DELUGE_ENABLED=false
DELUGE_NETWORK_MODE=host

original_message: Added Bazarr


message:
Portainer timefix
Removed TZ as not supported by Portainer and replaced with /etc/localtime:/etc/localtime:ro

diff:
@@ -5,9 +5,8 @@ services:
container_name: portainer
restart: always
network_mode: ${PORTAINER_NETWORK_MODE}
- environment:
- - TZ=${TZ}
volumes:
+ - /etc/localtime:/etc/localtime:ro
- /var/run/docker.sock:/var/run/docker.sock
- ${DOCKERCONFDIR}/portainer:/data
command: -H unix:///var/run/docker.sock


message: Added missing apt-transport-https dep package

diff:
@@ -7,7 +7,7 @@ run_apt() {
if [[ ${CI:-} != true ]] && [[ ${TRAVIS:-} != true ]]; then
apt-get -y dist-upgrade
fi
- apt-get -qq install curl git grep sed
+ apt-get -qq install curl git grep sed apt-transport-https
apt-get -y autoremove
apt-get -y autoclean
}